Dateien nach „d2s-discourse-map“ hochladen
This commit is contained in:
parent
b774d08881
commit
8ac7438b18
2 changed files with 275 additions and 0 deletions
275
d2s-discourse-map/d2s-discourse-map.php
Normal file
275
d2s-discourse-map/d2s-discourse-map.php
Normal 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']);
|
||||
BIN
d2s-discourse-map/d2s-discourse-map.zip
Normal file
BIN
d2s-discourse-map/d2s-discourse-map.zip
Normal file
Binary file not shown.
Loading…
Reference in a new issue