Dateien nach „d2s-discourse-map“ hochladen

This commit is contained in:
Michael Fuchs 2025-12-22 11:15:31 +00:00
parent b774d08881
commit 8ac7438b18
2 changed files with 275 additions and 0 deletions

View file

@ -0,0 +1,275 @@
<?php
/**
* Plugin Name: Discourse Tag Automapper + Admin UI
* Description: Mappt WP-Kategorien Discourse-Tags via Admin-Interface. Robuste Timing-Strategie + direkte Injection in den Discourse-Request-Body.
* Version: 1.6.3
* Author: Donau2Space
*/
if (!defined('ABSPATH')) exit;
class D2S_Discourse_Tag_Automapper {
const OPTION = 'd2s_discourse_tag_mapping';
const LOGFILE = 'd2s-automapper.log'; // wp-content/d2s-automapper.log
const LAST_PUB_TX = 'd2s_last_publish_post_id'; // transient key (short-lived)
public function __construct() {
add_action('admin_menu', [$this, 'add_menu']);
add_action('admin_init', [$this, 'maybe_boot_defaults']);
// ältere / alternative Filterpfade vom WP-Discourse-Plugin
add_filter('discourse_publish_post_params', [$this, 'apply_mapping_to_params'], 10, 2);
add_filter('discourse_publish_post_parameters', [$this, 'apply_mapping_to_params'], 10, 2);
// Robust: spät mappen, wenn Terms sicher dran sind
add_action('save_post', [$this, 'apply_mapping_on_save'], 999, 3);
add_action('wp_after_insert_post', [$this, 'apply_mapping_after_insert'], 10, 4);
// Final: tatsächlichen HTTP-Body patchen, der an Discourse geht
add_filter('wpdc_publish_body', [$this, 'inject_tags_into_publish_body'], 10, 2);
// Beim Statuswechsel publish → Post-ID kurz merken (für wpdc_publish_body ohne $_REQUEST)
add_action('transition_post_status', [$this, 'remember_post_on_publish'], 5, 3);
// Optional: Log beim Statuswechsel
add_action('transition_post_status', [$this, 'apply_mapping_on_status_change'], 20, 3);
}
public function maybe_boot_defaults() {
$opt = get_option(self::OPTION);
if ($opt === false) {
update_option(self::OPTION, [
'fallback_tag' => 'logbuch',
'category_map' => ['privatlog' => 'privatlog'],
], false);
}
}
public function add_menu() {
add_options_page(
'Discourse Tag Automapper',
'Discourse Tag Automapper',
'manage_options',
'd2s-discourse-tag-automapper',
[$this, 'render_page']
);
}
private function sanitize_tag($s) {
$s = is_string($s) ? trim($s) : '';
$s = strtolower($s);
$s = str_replace([',',';'], '', $s);
$s = preg_replace('/\s+/', '-', $s);
$s = preg_replace('/[^a-z0-9\-_]/', '', $s);
return $s;
}
private function handle_save() {
if (!current_user_can('manage_options')) return;
if (!isset($_POST['d2s_discourse_tag_nonce']) || !wp_verify_nonce($_POST['d2s_discourse_tag_nonce'], 'd2s_discourse_tag_save')) return;
$fallback = isset($_POST['fallback_tag']) ? $this->sanitize_tag(wp_unslash($_POST['fallback_tag'])) : '';
$raw_map = isset($_POST['category_map']) ? (array) $_POST['category_map'] : [];
$map = [];
foreach ($raw_map as $cat_id => $tag) {
$tag = $this->sanitize_tag(wp_unslash($tag));
$cat_id = intval($cat_id);
if ($cat_id > 0 && $tag !== '') {
$term = get_term($cat_id, 'category');
if ($term && !is_wp_error($term)) $map[$term->slug] = $tag;
}
}
update_option(self::OPTION, [
'fallback_tag' => $fallback !== '' ? $fallback : 'logbuch',
'category_map' => $map,
], false);
add_settings_error('d2s_discourse_tag_automapper', 'saved', 'Einstellungen gespeichert.', 'updated');
}
public function render_page() {
if ($_SERVER['REQUEST_METHOD'] === 'POST') $this->handle_save();
$opt = get_option(self::OPTION);
$fallback = $opt['fallback_tag'] ?? 'logbuch';
$map = (array)($opt['category_map'] ?? []);
$cats = get_categories(['hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC']);
settings_errors('d2s_discourse_tag_automapper'); ?>
<div class="wrap">
<h1>Discourse Tag Automapper</h1>
<form method="post">
<?php wp_nonce_field('d2s_discourse_tag_save', 'd2s_discourse_tag_nonce'); ?>
<table class="form-table" role="presentation">
<tr>
<th scope="row"><label for="fallback_tag">Fallback-Tag</label></th>
<td>
<input name="fallback_tag" id="fallback_tag" type="text" class="regular-text" value="<?php echo esc_attr($fallback); ?>" />
<p class="description">Genutzt, wenn keine Beitragkategorie ein Mapping hat (z. B. <code>logbuch</code>).</p>
</td>
</tr>
</table>
<h2 class="title">Kategorie Discourse-Tag</h2>
<p>Pro WP-Kategorie optional ein Discourse-Tag. Leer lassen = kein spezielles Tag Fallback greift.</p>
<table class="widefat striped">
<thead><tr><th style="width:35%">Kategorie</th><th>Discourse-Tag</th></tr></thead>
<tbody>
<?php foreach ($cats as $cat):
$slug = $cat->slug;
$current = $map[$slug] ?? '';
?>
<tr>
<td>
<strong><?php echo esc_html($cat->name); ?></strong>
<div class="description">Slug: <code><?php echo esc_html($slug); ?></code> (ID <?php echo intval($cat->term_id); ?>)</div>
</td>
<td>
<input type="text" name="category_map[<?php echo intval($cat->term_id); ?>]" value="<?php echo esc_attr($current); ?>" class="regular-text" placeholder="z. B. privatlog" />
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php submit_button('Speichern'); ?>
</form>
</div>
<?php
}
/* ===================== Kernlogik ===================== */
private function get_post_categories_debug($post_id) {
$terms = get_the_terms($post_id, 'category');
$list = [];
if (!empty($terms) && !is_wp_error($terms)) {
foreach ($terms as $t) $list[] = $t->slug;
}
return $list;
}
private function compute_tags_for_post($post) {
$opt = get_option(self::OPTION);
$fallback = $opt['fallback_tag'] ?? 'logbuch';
$map = (array)($opt['category_map'] ?? []);
$auto = $fallback;
$terms = get_the_terms($post, 'category');
if (!empty($terms) && !is_wp_error($terms)) {
usort($terms, fn($a,$b)=> strcmp($a->name, $b->name));
foreach ($terms as $t) {
if (isset($map[$t->slug]) && $map[$t->slug] !== '') { $auto = $map[$t->slug]; break; }
}
}
$tags = [$auto]; // kein Merge mit älteren Meta-Werten
$tags = array_map([$this, 'sanitize_tag'], $tags);
$tags = array_values(array_unique(array_filter($tags)));
$this->log('[compute] post_id='.$post->ID.' cats='.wp_json_encode($this->get_post_categories_debug($post->ID)).' -> tags='.wp_json_encode($tags));
return $tags;
}
/* ===== ältere Filterpfade: falls deine Version sie nutzt ===== */
public function apply_mapping_to_params($params, $post) {
if (!$post instanceof WP_Post) return $params;
$tags = $this->compute_tags_for_post($post);
$params['tags'] = $tags;
$params['tag_names'] = $tags;
$this->log('[params] post_id='.$post->ID.' tags='.wp_json_encode($tags));
return $params;
}
/* ===== Späte/robuste Wege ===== */
public function apply_mapping_on_save($post_id, $post, $update) {
if (wp_is_post_revision($post_id)) return;
if ($post->post_type !== 'post') return;
$tags = $this->compute_tags_for_post($post);
update_post_meta($post_id, 'wpdc_discourse_tags', implode(',', $tags));
update_post_meta($post_id, 'discourse_tags', implode(',', $tags));
$this->log('[save_post] post_id='.$post_id.' tags='.wp_json_encode($tags));
}
public function apply_mapping_after_insert($post_id, $post, $update, $post_before) {
if ($post->post_type !== 'post') return;
$tags = $this->compute_tags_for_post($post);
update_post_meta($post_id, 'wpdc_discourse_tags', implode(',', $tags));
update_post_meta($post_id, 'discourse_tags', implode(',', $tags));
$this->log('[after_insert] post_id='.$post_id.' tags='.wp_json_encode($tags));
}
public function remember_post_on_publish($new_status, $old_status, $post) {
if (!$post instanceof WP_Post || $post->post_type !== 'post') return;
if ($new_status === 'publish' && $old_status !== 'publish') {
// ID 30s merken reicht für den direkt anschließenden Publish-Request
set_transient(self::LAST_PUB_TX, intval($post->ID), 30);
$this->log('[remember] post_id='.$post->ID.' (publish transient gesetzt)');
}
}
public function apply_mapping_on_status_change($new_status, $old_status, $post) {
if (!$post instanceof WP_Post || $post->post_type !== 'post') return;
if ($new_status === 'publish' && $old_status !== 'publish') {
$this->apply_mapping_on_save($post->ID, $post, true);
$this->log('[status_change] post_id='.$post->ID.' '.$old_status.'→'.$new_status);
}
}
/* ===== Final: Publish-Body patchen (garantiert im Request) ===== */
public function inject_tags_into_publish_body($body, $remote_post_type) {
// 1) Versuche $_REQUEST (falls die WP-Discourse-Version das setzt)
$post_id = isset($_REQUEST['post_id']) ? intval($_REQUEST['post_id']) : (isset($_REQUEST['post']) ? intval($_REQUEST['post']) : 0);
// 2) Fallback: nimm die zuletzt veröffentlichte ID aus dem Transient
if (!$post_id) {
$post_id = intval(get_transient(self::LAST_PUB_TX));
if ($post_id) {
$this->log('[publish_body] fallback via transient post_id='.$post_id);
}
}
$post = $post_id ? get_post($post_id) : null;
if ($post instanceof WP_Post && $post->post_type === 'post') {
$tags = $this->compute_tags_for_post($post); // frisch, aus den aktuellen Kategorien
if (!empty($tags)) {
$body['tags'] = $tags;
$body['tag_names'] = $tags;
$this->log('[publish_body] post_id='.$post->ID.' type='.$remote_post_type.' cats='.wp_json_encode($this->get_post_categories_debug($post->ID)).' tags='.wp_json_encode($tags));
} else {
$this->log('[publish_body] post_id='.$post->ID.' type='.$remote_post_type.' tags=[] (leer)');
}
} else {
$this->log('[publish_body] kein gültiger Post-Kontext');
}
return $body;
}
private function log($line) {
$file = trailingslashit(WP_CONTENT_DIR) . self::LOGFILE;
if (file_exists($file) && filesize($file) > 200 * 1024) {
@unlink($file); // rotate
}
$date = gmdate('Y-m-d H:i:s');
@file_put_contents($file, '['.$date.' UTC] '.$line.PHP_EOL, FILE_APPEND);
}
public static function on_uninstall() {
delete_option(self::OPTION);
// Log bleibt liegen
}
}
new D2S_Discourse_Tag_Automapper();
register_uninstall_hook(__FILE__, ['D2S_Discourse_Tag_Automapper', 'on_uninstall']);

Binary file not shown.