'',
'upload' => '',
'eye' => '',
'check' => '',
'help' => '',
];
if (!isset($paths[$key])) return '';
return '';
}
function mkb_glossar_csv_fields() {
return [
'title' => __('Term title', 'mkb-glossar'),
'slug' => __('URL slug', 'mkb-glossar'),
'summary' => __('Short definition', 'mkb-glossar'),
'definition' => __('Full definition', 'mkb-glossar'),
'synonyms' => __('Synonyms', 'mkb-glossar'),
'topics' => __('Topics (slug::name | …)', 'mkb-glossar'),
'language' => __('Language code', 'mkb-glossar'),
'status' => __('Publication status', 'mkb-glossar'),
'related_terms' => __('Related terms (slugs)', 'mkb-glossar'),
'references' => __('Sources and references', 'mkb-glossar'),
'external_url' => __('External link destination', 'mkb-glossar'),
'use_external_target' => __('Use external link destination', 'mkb-glossar'),
'external_new_tab' => __('Open external destination in new tab', 'mkb-glossar'),
'external_nofollow' => __('External destination: nofollow', 'mkb-glossar'),
'external_sponsored' => __('External destination: sponsored', 'mkb-glossar'),
'external_ugc' => __('External destination: ugc', 'mkb-glossar'),
'case_sensitive' => __('Exact capitalization', 'mkb-glossar'),
'translation_group' => __('Translation group', 'mkb-glossar'),
];
}
function mkb_glossar_csv_aliases() {
return [
'title'=>['title','term','begriff','name','begriffstitel'],
'slug'=>['slug','url_slug','permalink'],
'summary'=>['summary','short_definition','kurzdefinition','excerpt'],
'definition'=>['definition','content','beschreibung','description','post_content'],
'synonyms'=>['synonyms','synonyme','alternative_names'],
'topics'=>['topics','themen','categories','kategorien'],
'language'=>['language','sprache','lang','locale'],
'status'=>['status','post_status','veroeffentlichung'],
'related_terms'=>['related_terms','related','verwandte_begriffe'],
'references'=>['references','sources','quellen','referenzen'],
'external_url'=>['external_url','external_link','externer_link'],
'use_external_target'=>['use_external_target','external_target','external_enabled'],
'external_new_tab'=>['external_new_tab','external_newtab','neuer_tab'],
'external_nofollow'=>['external_nofollow'],
'external_sponsored'=>['external_sponsored'],
'external_ugc'=>['external_ugc'],
'case_sensitive'=>['case_sensitive','match_case','gross_kleinschreibung'],
'translation_group'=>['translation_group','translation_key','uebersetzungsgruppe'],
];
}
function mkb_glossar_csv_default_map($headers) {
$result=[];
foreach (mkb_glossar_csv_aliases() as $field => $aliases) {
$result[$field]='';
foreach ($headers as $i=>$name) {
$value=strtolower(trim((string)$name));
$value=preg_replace('/[\s\-]+/u','_', $value);
if (in_array($value,$aliases,true)) { $result[$field]=(string)$i; break; }
}
}
return $result;
}
function mkb_glossar_csv_normalize_map($raw, $headers) {
$defaults=mkb_glossar_csv_default_map($headers);
if ($raw===null) return $defaults;
if (!is_array($raw)) return new WP_Error('mkb_csv_mapping', __('Invalid CSV field mapping.', 'mkb-glossar'));
$out=[]; $used=[];
foreach ($defaults as $field=>$default) {
$n=$raw[$field]??'';
if ($n==='') { $out[$field]=''; continue; }
if (!is_scalar($n) || !ctype_digit((string)$n) || (int)$n>=count($headers)) return new WP_Error('mkb_csv_mapping', __('Invalid CSV field mapping.', 'mkb-glossar'));
$n=(string)(int)$n;
if (in_array($n,$used,true)) return new WP_Error('mkb_csv_mapping_duplicate', __('A CSV column can only be assigned once.', 'mkb-glossar'));
$out[$field]=$n; $used[]=$n;
}
return $out;
}
function mkb_glossar_csv_cell($row, $mapping, $key) {
$index = $mapping[$key]??'';
return $index==='' ? '' : (string)($row[(int)$index]??'');
}
function mkb_glossar_csv_upload($file) {
if (!is_array($file) || !isset($file['error']) || $file['error']!==UPLOAD_ERR_OK || !isset($file['tmp_name'],$file['name'],$file['size'])) {
return new WP_Error('mkb_csv_upload', __('Please select a valid CSV file.', 'mkb-glossar'));
}
if (!is_scalar($file['name']) || strtolower(pathinfo((string)$file['name'],PATHINFO_EXTENSION))!=='csv') {
return new WP_Error('mkb_csv_extension', __('Only .csv files are accepted.', 'mkb-glossar'));
}
if (!is_int($file['size']) && !ctype_digit((string)$file['size'])) return new WP_Error('mkb_csv_size', __('Invalid file size.', 'mkb-glossar'));
if ((int)$file['size']<2 || (int)$file['size']>8*1024*1024) return new WP_Error('mkb_csv_size', __('The CSV must be smaller than 8 MB.', 'mkb-glossar'));
if (!is_string($file['tmp_name']) || !is_uploaded_file($file['tmp_name'])) return new WP_Error('mkb_csv_upload', __('The CSV upload could not be verified.', 'mkb-glossar'));
return $file['tmp_name'];
}
/** Streaming CSV requires native PHP fgetcsv/fputcsv. These handles exclusively
* read PHP-verified uploaded temp files or write php://output; no permanent
* WordPress filesystem files are modified. WP_Filesystem cannot express a
* bounded, quoted-multiline RFC4180 CSV stream.
*/
function mkb_glossar_csv_open_stream($path, $mode) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- Verified temporary CSV upload or php://output streaming response.
return @fopen($path, $mode);
}
function mkb_glossar_csv_close_stream($handle) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Close the temporary stream opened exclusively for CSV processing.
return fclose($handle);
}
function mkb_glossar_csv_write_bom($stream) {
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- UTF-8 BOM is written to php://output, not the persistent filesystem.
return fwrite($stream, "\xEF\xBB\xBF");
}
/** Parser is separated from upload validation so isolated tests can exercise it. */
function mkb_glossar_csv_parse($path) {
$fh=mkb_glossar_csv_open_stream($path,'rb');
if (!$fh) return new WP_Error('mkb_csv_read', __('The CSV could not be read.', 'mkb-glossar'));
$first=fgets($fh,65536);
if (!is_string($first)) {mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_header', __('The CSV needs a header row.', 'mkb-glossar'));}
$first=preg_replace('/^\xEF\xBB\xBF/','',$first);
$counts=[];
foreach ([',',';',"\t"] as $delimiter) $counts[$delimiter]=count(str_getcsv($first,$delimiter,'"',''));
arsort($counts);
$delimiter=(string)array_key_first($counts);
rewind($fh);
$headers=fgetcsv($fh,65536,$delimiter,'"','');
if (!is_array($headers) || count($headers)<1 || count($headers)>50) {mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_header', __('The CSV header is invalid.', 'mkb-glossar'));}
$headers[0]=preg_replace('/^\xEF\xBB\xBF/','',(string)$headers[0]);
$headers=array_map('trim',$headers);
if (count(array_filter($headers,static fn($x)=>$x!==''))!==count($headers) || count(array_unique($headers))!==count($headers)) {
mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_header', __('CSV column names must be unique and nonempty.', 'mkb-glossar'));
}
$rows=[]; $line=1;
while (($row=fgetcsv($fh,65536,$delimiter,'"',''))!==false) {
$line++;
if ($row===[null]) continue;
if (count($rows)>=2000) {mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_rows', __('Use no more than 2,000 rows per import.', 'mkb-glossar'));}
/* translators: %d is the one-based CSV row number. */
if (count($row)!==count($headers)) {mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_columns', sprintf(__('Row %d has a different number of columns.', 'mkb-glossar'),$line));}
foreach ($row as $value) {
if (!is_string($value) || strlen($value)>50000 || !preg_match('//u',$value)) {
/* translators: %d is the one-based CSV row number. */
mkb_glossar_csv_close_stream($fh);return new WP_Error('mkb_csv_encoding', sprintf(__('Row %d contains invalid or oversized text. Use UTF-8.', 'mkb-glossar'),$line));
}
}
$rows[]=['line'=>$line,'cells'=>$row];
}
mkb_glossar_csv_close_stream($fh);
if (!$rows) return new WP_Error('mkb_csv_empty', __('No glossary rows were found.', 'mkb-glossar'));
return ['headers'=>$headers,'rows'=>$rows,'delimiter'=>$delimiter];
}
/** CSV formula neutralization for spreadsheet viewers, round-tripped on import. */
function mkb_glossar_csv_escape_cell($value) {
$value=(string)$value;
return preg_match('/^[\s]*[=+@\-]/u',$value) ? "'".$value : $value;
}
function mkb_glossar_csv_unescape_cell($value) {
$value=(string)$value;
return preg_match("/^'[\\s]*[=+@\\-]/u",$value) ? substr($value,1) : $value;
}
function mkb_glossar_csv_language($raw) {
$raw=trim((string)$raw);
$active=mkb_glossar_wpml_enabled() || mkb_glossar_polylang_enabled();
if (!$active) return $raw==='' || $raw==='site-default' ? 'site-default' : null;
$code=($raw!=='' && $raw!=='site-default')?$raw:mkb_glossar_default_language();
return in_array($code,mkb_glossar_url_languages(),true) ? $code : null;
}
function mkb_glossar_csv_split($value) {
return array_values(array_unique(array_filter(array_map('trim', explode('|',(string)$value)),static fn($v)=>$v!=='')));
}
function mkb_glossar_csv_existing_index() {
$posts=get_posts(['post_type'=>'mkb_glossar','post_status'=>['publish','draft','pending','private','future'],
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Gather entries from every language for CSV identity reconciliation.
'posts_per_page'=>-1,'orderby'=>'ID','order'=>'ASC','suppress_filters'=>true]);
$result=['names'=>[],'slugs'=>[]];
foreach ($posts as $post) {
$lang=mkb_glossar_post_language($post->ID);
if ($lang===null) continue;
$slug=(string)$post->post_name;
if ($slug!=='') $result['slugs'][$lang][$slug]=(int)$post->ID;
$names=array_merge([get_the_title($post->ID)],preg_split('/\r\n|\r|\n/',(string)get_post_meta($post->ID,'mkb_glossar_synonyms',true)) ?: []);
foreach ($names as $name) {
$key=mkb_glossar_identity_key($name);
if ($key!=='') $result['names'][$lang][$key]=(int)$post->ID;
}
}
return $result;
}
function mkb_glossar_csv_normalize_row($cells, $map, $line) {
$read=static fn($key) => mkb_glossar_csv_unescape_cell(mkb_glossar_csv_cell($cells,$map,$key));
$title=sanitize_text_field($read('title'));
/* translators: %d is the one-based CSV row number. */
if ($title==='' || (function_exists('mb_strlen')?mb_strlen($title,'UTF-8'):strlen($title))>250) return new WP_Error('mkb_csv_title',sprintf(__('Row %d needs a term title (up to 250 characters).','mkb-glossar'),$line));
$slug=$read('slug'); $slug=$slug!==''?sanitize_title($slug):sanitize_title($title);
/* translators: %d is the one-based CSV row number. */
if ($slug==='') return new WP_Error('mkb_csv_slug',sprintf(__('Row %d has an invalid URL slug.','mkb-glossar'),$line));
$language=mkb_glossar_csv_language($read('language'));
/* translators: %d is the one-based CSV row number. */
if ($language===null) return new WP_Error('mkb_csv_language',sprintf(__('Row %d has an unsupported language code.','mkb-glossar'),$line));
$status=sanitize_key($read('status'));
if ($status==='' || !in_array($status,['publish','draft','pending','private'],true)) $status='draft';
$summary=sanitize_textarea_field($read('summary'));
$synonyms=sanitize_textarea_field($read('synonyms'));
$raw_definition=$read('definition');
$definition=current_user_can('unfiltered_html')?$raw_definition:wp_kses_post($raw_definition);
$editorial=mkb_glossar_editorial_prepare([
'term_references'=>$read('references'),
'term_external_url'=>$read('external_url'),
'term_use_external'=>in_array(strtolower($read('use_external_target')),['1','true','yes','ja'],true)?'1':'0',
'term_external_newtab'=>($map['external_new_tab']??'')==='' ? '1' : (in_array(strtolower($read('external_new_tab')),['1','true','yes','ja'],true)?'1':'0'),
'term_external_nofollow'=>in_array(strtolower($read('external_nofollow')),['1','true','yes','ja'],true)?'1':'0',
'term_external_sponsored'=>in_array(strtolower($read('external_sponsored')),['1','true','yes','ja'],true)?'1':'0',
'term_external_ugc'=>in_array(strtolower($read('external_ugc')),['1','true','yes','ja'],true)?'1':'0',
'term_case_sensitive'=>in_array(strtolower($read('case_sensitive')),['1','true','yes','ja'],true)?'1':'0',
],0,$language,false);
/* translators: %1$d is the CSV row number, %2$s is the validation error message. */
if (is_wp_error($editorial)) return new WP_Error('mkb_csv_editorial',sprintf(__('Row %1$d: %2$s','mkb-glossar'),$line,$editorial->get_error_message()));
$topics=[];
foreach (mkb_glossar_csv_split($read('topics')) as $item) {
$pair=explode('::',$item,4);
$slug_part=sanitize_title($pair[0]);
if ($slug_part==='') continue;
$topics[$slug_part]=[
'name'=>isset($pair[1])?sanitize_text_field($pair[1]):ucwords(str_replace('-',' ',$slug_part)),
'parent_slug'=>isset($pair[2])?sanitize_title($pair[2]):'',
'parent_name'=>isset($pair[3])?sanitize_text_field($pair[3]):'',
];
}
$related=[];
foreach (mkb_glossar_csv_split($read('related_terms')) as $part) {
$related_slug=sanitize_title($part);
if ($related_slug!=='') $related[]=$related_slug;
}
/* translators: %d is the one-based CSV row number. */
if (count($related)>12) return new WP_Error('mkb_csv_related',sprintf(__('Row %d has too many related terms.','mkb-glossar'),$line));
$translation_group=trim($read('translation_group'));
/* translators: %d is the one-based CSV row number. */
if ($translation_group!=='' && !preg_match('/^[A-Za-z0-9_.:-]{1,100}$/D',$translation_group)) return new WP_Error('mkb_csv_group',sprintf(__('Row %d has an invalid translation group.','mkb-glossar'),$line));
return compact('title','slug','language','status','summary','synonyms','definition','editorial','topics','related','translation_group') + ['line'=>$line];
}
/** One shared preflight for preview and final import; final import must revalidate. */
function mkb_glossar_csv_review($parsed, $mapping, $mode='skip') {
if (($mapping['title']??'')==='') return new WP_Error('mkb_csv_title_mapping', __('Assign a CSV column to the term title.', 'mkb-glossar'));
$index=mkb_glossar_csv_existing_index();
$seen=[]; $groups=[]; $entries=[]; $counts=['create'=>0,'update'=>0,'skip'=>0,'error'=>0];
foreach ($parsed['rows'] as $item) {
$record=mkb_glossar_csv_normalize_row($item['cells'],$mapping,$item['line']);
if (is_wp_error($record)) {
$entries[]=['line'=>$item['line'],'title'=>'','action'=>'error','message'=>$record->get_error_message()];$counts['error']++;continue;
}
$lang=$record['language'];$name=mkb_glossar_identity_key($record['title']);
$existing_title_id=$index['names'][$lang][$name]??0;
$slug_id=$index['slugs'][$lang][$record['slug']]??0;
$collision=0;
if ($existing_title_id) {
$actual=mkb_glossar_identity_key(get_the_title($existing_title_id));
if ($actual!==$name) $collision=$existing_title_id;
}
if ($slug_id && $existing_title_id && $slug_id!==$existing_title_id) $collision=$slug_id;
if ($slug_id && !$existing_title_id) $collision=$slug_id;
foreach (array_merge([$record['title']],preg_split('/\r\n|\r|\n/',$record['synonyms'])?:[]) as $alias) {
$key=mkb_glossar_identity_key($alias);
if ($key==='') continue;
$other=$index['names'][$lang][$key]??0;
if ($other && $other!==$existing_title_id) $collision=$other;
if (isset($seen[$lang][$key])) $collision=-1;
$seen[$lang][$key]=true;
}
$group=$record['translation_group'];
if ($group!=='' && isset($groups[$group][$lang])) $collision=-2;
if ($group!=='') $groups[$group][$lang]=true;
$action=$collision?'error':($existing_title_id?($mode==='update'?'update':'skip'):'create');
if ($action==='create') {
// Reserve future title, synonyms and slug to detect subsequent rows.
$index['slugs'][$lang][$record['slug']]=-1;
}
$entries[]=['line'=>$item['line'],'title'=>$record['title'],'action'=>$action,
'message'=>$collision ? __('Conflicting term, synonym or URL slug. No changes will be made to this row.', 'mkb-glossar') :
($action==='skip'?__('Already exists; will be skipped.', 'mkb-glossar') : ''),
'existing_id'=>$existing_title_id,'record'=>$record];
$counts[$action]++;
}
return ['entries'=>$entries,'counts'=>$counts,'mapping'=>$mapping];
}
function mkb_glossar_csv_topic_id($slug,$name,$language,$allow_create,$parent_slug='',$parent_name='') {
$terms=get_terms(['taxonomy'=>'mkb_glossar_topic','hide_empty'=>false,'slug'=>$slug,'suppress_filter'=>true]);
if (is_wp_error($terms)) return 0;
foreach ($terms as $term) {
if (!(mkb_glossar_wpml_enabled() || mkb_glossar_polylang_enabled()) || mkb_glossar_topic_language($term->term_id)===$language) return (int)$term->term_id;
}
if (!$allow_create) return 0;
$parent_id=0;
if ($parent_slug!=='' && $parent_slug!==$slug) {
$parent_id=mkb_glossar_csv_topic_id($parent_slug,$parent_name!==''?$parent_name:ucwords(str_replace('-',' ',$parent_slug)),$language,$allow_create);
}
$created=wp_insert_term($name,'mkb_glossar_topic',['slug'=>$slug,'parent'=>$parent_id]);
if (is_wp_error($created)) return 0;
$id=(int)$created['term_id'];
if (mkb_glossar_polylang_enabled() && function_exists('pll_set_term_language')) pll_set_term_language($id,$language);
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration.
elseif (mkb_glossar_wpml_enabled()) do_action('wpml_set_element_language_details',['element_id'=>$id,'element_type'=>'tax_mkb_glossar_topic','trid'=>false,'language_code'=>$language]);
return $id;
}
function mkb_glossar_csv_assign_language($id,$language) {
if ($language==='site-default') return;
if (mkb_glossar_polylang_enabled() && function_exists('pll_set_post_language')) pll_set_post_language($id,$language);
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration.
elseif (mkb_glossar_wpml_enabled()) do_action('wpml_set_element_language_details',['element_id'=>$id,'element_type'=>'post_mkb_glossar','trid'=>false,'language_code'=>$language]);
}
function mkb_glossar_csv_apply($review,$mapping,$options) {
$report=['created'=>0,'updated'=>0,'skipped'=>0,'errors'=>0,'warnings'=>[],'saved'=>[]];
$related_pending=[]; $topic_cache=[]; $translation_pending=[];
foreach ($review['entries'] as $entry) {
$action=$entry['action'];
if ($action==='skip') { $report['skipped']++;continue; }
if ($action==='error') { $report['errors']++;continue; }
$r=$entry['record'];
$status=($options['drafts'] && $action==='create')?'draft':$r['status'];
if (!current_user_can('publish_posts') && $status==='publish') $status='pending';
$fields=['post_type'=>'mkb_glossar','post_status'=>$status,'post_title'=>$r['title']];
if ($action==='create') $fields['post_name']=$r['slug'];
else $fields['ID']=(int)$entry['existing_id'];
if (($mapping['definition']??'')!=='') $fields['post_content']=$r['definition'];
if ($action==='update' && ($options['drafts'] || ($mapping['status']??'')==='')) unset($fields['post_status']);
$id=wp_insert_post(wp_slash($fields),true);
/* translators: %d is the one-based CSV row number. */
if (is_wp_error($id) || !$id) { $report['errors']++;$report['warnings'][]=sprintf(__('Row %d could not be saved.','mkb-glossar'),$entry['line']);continue; }
if ($action==='create') mkb_glossar_csv_assign_language($id,$r['language']);
if (($mapping['summary']??'')!=='' || $action==='create') update_post_meta($id,'mkb_glossar_summary',$r['summary']);
if (($mapping['synonyms']??'')!=='' || $action==='create') update_post_meta($id,'mkb_glossar_synonyms',$r['synonyms']);
$meta_mapping=['references'=>'mkb_glossar_references','external_url'=>'mkb_glossar_external_url',
'use_external_target'=>'mkb_glossar_use_external','external_new_tab'=>'mkb_glossar_external_newtab','external_nofollow'=>'mkb_glossar_external_nofollow','external_sponsored'=>'mkb_glossar_external_sponsored','external_ugc'=>'mkb_glossar_external_ugc','case_sensitive'=>'mkb_glossar_case_sensitive'];
foreach ($meta_mapping as $field=>$meta) {
if (($mapping[$field]??'')!=='' || $action==='create') update_post_meta($id,$meta,$r['editorial'][$meta]);
}
if (($mapping['topics']??'')!=='') {
$term_ids=[]; $missing_topic=false;
foreach ($r['topics'] as $slug=>$topic) {
$key=$r['language'].'/'.$slug;
if (!array_key_exists($key,$topic_cache)) $topic_cache[$key]=mkb_glossar_csv_topic_id($slug,$topic['name'],$r['language'],$options['create_topics'],$topic['parent_slug'],$topic['parent_name']);
if ($topic_cache[$key]) $term_ids[]=$topic_cache[$key];
else {
$missing_topic=true;
/* translators: %1$d is the CSV row number, %2$s is the topic name. */
$report['warnings'][]=sprintf(__('Row %1$d: topic “%2$s” was not assigned. Existing topic assignments were preserved.','mkb-glossar'),$entry['line'],$topic['name']);
}
}
// Never replace existing assignments with a partial/incomplete import.
if (!$missing_topic) {
$assigned=wp_set_object_terms($id,$term_ids,'mkb_glossar_topic');
/* translators: %d is the one-based CSV row number. */
if (is_wp_error($assigned)) $report['warnings'][]=sprintf(__('Row %d: topic assignments could not be saved.','mkb-glossar'),$entry['line']);
}
}
if (($mapping['related_terms']??'')!=='') $related_pending[]=['id'=>$id,'record'=>$r];
if (($mapping['translation_group']??'')!=='' && $r['translation_group']!=='')
$translation_pending[$r['translation_group']][$r['language']]=$id;
$report[$action==='create'?'created':'updated']++;
$report['saved'][]=$id;
}
// Relations are resolved after all terms were created, regardless of CSV order.
if ($related_pending) {
$all=mkb_glossar_csv_existing_index();
foreach ($related_pending as $pending) {
$ids=[]; $r=$pending['record'];
foreach ($r['related'] as $slug) {
$id=$all['slugs'][$r['language']][$slug]??0;
/* translators: %1$d is the CSV row number, %2$s is the related term slug. */
if (!$id || $id===$pending['id']) $report['warnings'][]=sprintf(__('Row %1$d: related term “%2$s” was not found.','mkb-glossar'),$r['line'],$slug);
else $ids[]=$id;
}
if (count($ids)===count($r['related'])) update_post_meta($pending['id'],'mkb_glossar_related_ids',implode(',',array_unique($ids)));
}
}
if ($translation_pending) {
foreach ($translation_pending as $group=>$translations) {
if (count($translations)<2) continue;
if (mkb_glossar_polylang_enabled() && function_exists('pll_save_post_translations')) {
// Keep already-linked languages that are not present in this CSV.
// Partial imports must not dismantle existing translation groups.
$complete=$translations;
if (function_exists('pll_get_post_translations')) {
foreach ($translations as $id) {
foreach ((array)pll_get_post_translations((int)$id) as $lang=>$linked_id) {
if (!isset($complete[$lang]) && (int)$linked_id>0) $complete[$lang]=(int)$linked_id;
}
}
}
pll_save_post_translations($complete);
} elseif (mkb_glossar_wpml_enabled()) {
$first=(int)reset($translations);
// 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,$first,'post_mkb_glossar');
if (!$trid) {
/* translators: %s is the translation group identifier. */
$report['warnings'][]=sprintf(__('Translation group “%s” could not be linked.','mkb-glossar'),$group);
continue;
}
foreach ($translations as $lang=>$id) {
if ((int)$id===$first) continue;
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Required third-party WPML/Elementor API hook name; changing it would break integration.
do_action('wpml_set_element_language_details',['element_id'=>(int)$id,'element_type'=>'post_mkb_glossar','trid'=>$trid,'language_code'=>$lang]);
}
}
}
}
mkb_glossar_reset_request_cache();
return $report;
}
function mkb_glossar_csv_translation_group($id) {
if (mkb_glossar_polylang_enabled() && function_exists('pll_get_post_translations')) {
$linked=(array)pll_get_post_translations((int)$id);
$ids=array_filter(array_map('absint',$linked));
return count($ids)>1?'group-'.min($ids):'';
}
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)$id,'post_mkb_glossar');
return $trid ? 'group-'.$trid : '';
}
return '';
}
function mkb_glossar_csv_export_rows() {
$posts=get_posts(['post_type'=>'mkb_glossar','post_status'=>['publish','draft','pending','private'],
// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- Gather entries from every language for CSV identity reconciliation.
'posts_per_page'=>-1,'orderby'=>'ID','order'=>'ASC','suppress_filters'=>true]);
$rows=[];
foreach ($posts as $post) {
$related=[];
foreach (mkb_glossar_editorial_related_ids($post->ID) as $id) {
$target=get_post($id);if ($target) $related[]=$target->post_name;
}
$topics=[];
$terms=wp_get_post_terms($post->ID,'mkb_glossar_topic');
if (!is_wp_error($terms)) foreach ($terms as $term) {
$parent=(int)($term->parent??0);
$parent_term=$parent?get_term($parent,'mkb_glossar_topic'):null;
$suffix=($parent_term && !is_wp_error($parent_term))?'::'.$parent_term->slug.'::'.$parent_term->name:'';
$topics[]=$term->slug.'::'.$term->name.$suffix;
}
$rows[]=[
$post->post_title,$post->post_name,(string)get_post_meta($post->ID,'mkb_glossar_summary',true),
$post->post_content,(string)get_post_meta($post->ID,'mkb_glossar_synonyms',true),
implode('|',$topics),mkb_glossar_post_language($post->ID)??'', $post->post_status,implode('|',$related),
(string)get_post_meta($post->ID,'mkb_glossar_references',true),
(string)get_post_meta($post->ID,'mkb_glossar_external_url',true),
(string)get_post_meta($post->ID,'mkb_glossar_use_external',true)==='1'?'1':'0',
(string)get_post_meta($post->ID,'mkb_glossar_external_newtab',true)==='0'?'0':'1',
(string)get_post_meta($post->ID,'mkb_glossar_external_nofollow',true)==='1'?'1':'0',
(string)get_post_meta($post->ID,'mkb_glossar_external_sponsored',true)==='1'?'1':'0',
(string)get_post_meta($post->ID,'mkb_glossar_external_ugc',true)==='1'?'1':'0',
(string)get_post_meta($post->ID,'mkb_glossar_case_sensitive',true)==='1'?'1':'0',
mkb_glossar_csv_translation_group($post->ID),
];
}
return $rows;
}
/** The sample always uses the same stable column keys as a real export. */
function mkb_glossar_csv_sample_row() {
return [
'title' => 'Beispielbegriff',
'slug' => 'beispielbegriff',
'summary' => 'Eine kurze Erklärung des Begriffs.',
'definition' => 'Hier steht die ausführliche Beschreibung. Ersetze diesen Beispieltext durch deinen eigenen Inhalt.',
'synonyms' => 'Alternativer Begriff',
'topics' => 'beispiele::Beispiele',
'language' => '',
'status' => 'draft',
'related_terms' => '',
'references' => '',
'external_url' => '',
'use_external_target' => '0',
'external_new_tab' => '1',
'external_nofollow' => '0',
'external_sponsored' => '0',
'external_ugc' => '0',
'case_sensitive' => '0',
'translation_group' => '',
];
}
/** Browser download: 18 current columns, UTF-8 BOM, semicolon and one draft example. */
add_action('admin_post_mkb_glossar_csv_template', static function() {
if (!current_user_can('manage_options')) wp_die(esc_html__('Not allowed.', 'mkb-glossar'), 403);
check_admin_referer('mkb_glossar_csv_template');
if (headers_sent()) wp_die(esc_html__('CSV download cannot start after headers were sent.', 'mkb-glossar'));
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="mkb-glossar-muster.csv"');
header('X-Content-Type-Options: nosniff');
$output = mkb_glossar_csv_open_stream('php://output', 'wb');
mkb_glossar_csv_write_bom($output);
$keys = array_keys(mkb_glossar_csv_fields());
$sample = mkb_glossar_csv_sample_row();
fputcsv($output, $keys, ';', '"', '');
fputcsv($output, array_map('mkb_glossar_csv_escape_cell', array_values(array_replace(array_fill_keys($keys, ''), $sample))), ';', '"', '');
mkb_glossar_csv_close_stream($output);
exit;
});
add_action('admin_post_mkb_glossar_csv_export', static function() {
if (!current_user_can('manage_options')) wp_die(esc_html__('Not allowed.','mkb-glossar'),403);
check_admin_referer('mkb_glossar_csv_export');
if (headers_sent()) wp_die(esc_html__('CSV export cannot start after headers were sent.','mkb-glossar'));
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="mkb-glossar-'.gmdate('Y-m-d-His').'.csv"');
header('X-Content-Type-Options: nosniff');
$output=mkb_glossar_csv_open_stream('php://output','wb');
mkb_glossar_csv_write_bom($output);
fputcsv($output,array_keys(mkb_glossar_csv_fields()),';','"','');
foreach (mkb_glossar_csv_export_rows() as $row) fputcsv($output,array_map('mkb_glossar_csv_escape_cell',$row),';','"','');
mkb_glossar_csv_close_stream($output);
exit;
});
function mkb_glossar_csv_options($input) {
return [
'mode'=>isset($input['mode']) && is_scalar($input['mode']) && (string)$input['mode']==='update' ? 'update' : 'skip',
'drafts'=>!empty($input['drafts']) && is_scalar($input['drafts']) && (string)$input['drafts']==='1',
'create_topics'=>!empty($input['create_topics']) && is_scalar($input['create_topics']) && (string)$input['create_topics']==='1',
];
}
/**
* A CSV mapping is a small, known-key index dictionary, not arbitrary POST data.
* This parser is called only after the AJAX administrator nonce has passed.
*/
function mkb_glossar_csv_request_mapping($request) {
if (!is_array($request) || !array_key_exists('map', $request)) return null;
if (!is_array($request['map'])) {
return new WP_Error('mkb_csv_mapping', __('Invalid CSV field mapping.', 'mkb-glossar'));
}
$fields = mkb_glossar_csv_fields();
if (count($request['map']) > count($fields)) {
return new WP_Error('mkb_csv_mapping', __('Invalid CSV field mapping.', 'mkb-glossar'));
}
$result = [];
foreach ($request['map'] as $field => $column) {
if (!is_string($field) || !array_key_exists($field, $fields) || !is_scalar($column)) {
return new WP_Error('mkb_csv_mapping', __('Invalid CSV field mapping.', 'mkb-glossar'));
}
$result[$field] = sanitize_text_field(wp_unslash((string) $column));
}
return $result;
}
function mkb_glossar_csv_ajax_init() {
if (!current_user_can('manage_options')) wp_send_json_error(['message'=>__('Not allowed.','mkb-glossar')],403);
check_ajax_referer('mkb_glossar_csv','nonce');
// Uploaded PHP file descriptors must not be text-sanitized: validate error,
// size, suffix and is_uploaded_file() before ever opening the temp path.
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- PHP-managed upload descriptor is validated as a structure and verified with is_uploaded_file().
$tmp=mkb_glossar_csv_upload($_FILES['csv']??null);
if (is_wp_error($tmp)) wp_send_json_error(['message'=>$tmp->get_error_message()],400);
$parsed=mkb_glossar_csv_parse($tmp);
if (is_wp_error($parsed)) wp_send_json_error(['message'=>$parsed->get_error_message()],400);
// The file and field mapping are intentionally re-validated for BOTH
// preview and import; no client-supplied preview mapping is trusted.
$raw_map = mkb_glossar_csv_request_mapping($_POST);
if (is_wp_error($raw_map)) wp_send_json_error(['message'=>$raw_map->get_error_message()],400);
$map=mkb_glossar_csv_normalize_map($raw_map,$parsed['headers']);
if (is_wp_error($map)) wp_send_json_error(['message'=>$map->get_error_message()],400);
$options=mkb_glossar_csv_options($_POST);
$review=mkb_glossar_csv_review($parsed,$map,$options['mode']);
if (is_wp_error($review)) wp_send_json_error(['message'=>$review->get_error_message(),'headers'=>$parsed['headers'],'mapping'=>$map],400);
// The import's review token is read only inside this verified AJAX entry.
$review_token = isset($_POST['review_token']) && is_string($_POST['review_token'])
? sanitize_text_field(wp_unslash($_POST['review_token'])) : '';
return compact('tmp','parsed','map','options','review','review_token');
}
add_action('wp_ajax_mkb_glossar_csv_preview', static function() {
$data=mkb_glossar_csv_ajax_init();
$token=wp_generate_password(30,false,false);
set_transient('mkb_glossar_csv_'.get_current_user_id().'_'.$token,[
'sha256'=>hash_file('sha256',$data['tmp']),'mapping'=>$data['map'],'options'=>$data['options']
],15*MINUTE_IN_SECONDS);
$sample=[];
foreach ($data['review']['entries'] as $row) $sample[]=[
'line'=>$row['line'],'title'=>$row['title'],'action'=>$row['action'],'message'=>$row['message']
];
wp_send_json_success(['token'=>$token,'headers'=>$data['parsed']['headers'],'mapping'=>$data['map'],
'counts'=>$data['review']['counts'],'total'=>count($data['review']['entries']),'sample'=>$sample]);
});
add_action('wp_ajax_mkb_glossar_csv_import', static function() {
$data=mkb_glossar_csv_ajax_init();
$token = $data['review_token'];
if (!preg_match('/^[A-Za-z0-9]{30}$/D',$token)) wp_send_json_error(['message'=>__('Please preview the CSV before importing.','mkb-glossar')],409);
$key='mkb_glossar_csv_'.get_current_user_id().'_'.$token;
$reviewed=get_transient($key);
if (!is_array($reviewed) || !hash_equals((string)$reviewed['sha256'],hash_file('sha256',$data['tmp'])) ||
$reviewed['mapping']!==$data['map'] || $reviewed['options']!==$data['options']) {
wp_send_json_error(['message'=>__('The file or import choices changed. Please preview again.','mkb-glossar')],409);
}
delete_transient($key);
if ($data['review']['counts']['error']>0) wp_send_json_error(['message'=>__('Please correct all conflicting or invalid rows before importing. Nothing was saved.','mkb-glossar')],409);
$report=mkb_glossar_csv_apply($data['review'],$data['map'],$data['options']);
wp_send_json_success($report);
});
add_action('admin_menu',static function(){
add_submenu_page('edit.php?post_type=mkb_glossar',__('CSV import and export','mkb-glossar'),__('Import / Export','mkb-glossar'),
'manage_options','mkb-glossar-data','mkb_glossar_csv_page');
},25);
function mkb_glossar_csv_page() {
if (!current_user_can('manage_options')) wp_die(esc_html__('Not allowed.','mkb-glossar'),403);
?>
admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('mkb_glossar_csv'),
'fields'=>mkb_glossar_csv_fields(),
'none'=>__('— Do not import this field —','mkb-glossar'),
'actions'=>['create'=>__('New','mkb-glossar'),'update'=>__('Update','mkb-glossar'),
'skip'=>__('Skip','mkb-glossar'),'error'=>__('Conflict','mkb-glossar')],
'preview'=>__('Review before importing','mkb-glossar'),
'finished'=>__('Import finished.','mkb-glossar'),
'failed'=>__('The import failed. No changes were made if the review was not confirmed.','mkb-glossar'),
'counts'=>__('New / Update / Skip / Conflict','mkb-glossar'),
'line'=>__('Row','mkb-glossar'),
'done'=>__('Created / Updated / Skipped / Errors','mkb-glossar'),
'confirm'=>__('Import the reviewed changes now? This can create or update glossary terms.','mkb-glossar'),
'rows'=>__('rows','mkb-glossar'),'status'=>__('Status','mkb-glossar'),'details'=>__('Details','mkb-glossar'),
'showing'=>__('Showing rows','mkb-glossar'),
'previous'=>__('Previous results','mkb-glossar'), 'next'=>__('Next results','mkb-glossar'),
'correctConflicts'=>__('Please correct the conflicting CSV rows before importing.','mkb-glossar'),
'selectFile'=>__('Please select a CSV file.','mkb-glossar'),'checking'=>__('Checking CSV…','mkb-glossar'),
'importing'=>__('Importing reviewed data…','mkb-glossar'),'warnings'=>__('warnings in total','mkb-glossar'),
]).';','before');
});