Wp2ghost/WPPlugin/rbrExportRestApi.php

418 lines
13 KiB
PHP
Raw Permalink Normal View History

2026-09-20 08:31:23 +02:00
<?php
/**
* Plugin Name: RBR Export to Ghost
* Plugin URI:
* Description: Exportiert WordPress-Beiträge und Medien über eine REST-API für den Import in Ghost CMS.
* Version: 1.0.0
* Author: Roland Brunner
* Author URI:
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: rbr-export-ghost
* Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit;
}
define('RBR_EXPORT_VERSION', '1.0.0');
define('RBR_EXPORT_PATH', plugin_dir_path(__FILE__));
define('RBR_EXPORT_URL', plugin_dir_url(__FILE__));
register_activation_hook(__FILE__, 'rbr_export_activate');
register_deactivation_hook(__FILE__, 'rbr_export_deactivate');
function rbr_export_activate() {
flush_rewrite_rules();
}
function rbr_export_deactivate() {
flush_rewrite_rules();
}
/**
* Sanitisiert den per_page-Wert und bewahrt -1 für "alle".
*/
function rbr_sanitize_per_page($value) {
return (int) $value;
}
add_action('rest_api_init', function () {
// 1. Endpunkt: Alle Post-IDs abrufen
register_rest_route('custom/v1', '/posts-ids', array(
'methods' => 'GET',
'callback' => 'rbr_get_all_post_ids',
'permission_callback' => '__return_true',
'args' => array(
'post_type' => array(
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
),
'per_page' => array(
'default' => -1,
'sanitize_callback' => 'rbr_sanitize_per_page',
),
),
));
// 2. Endpunkt: Content & Medien für eine bestimmte Post-ID
register_rest_route('custom/v1', '/post-details/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_post_details_with_media',
'permission_callback' => '__return_true',
));
// 3. Endpunkt: Kategorien/Tags für alle Beiträge
register_rest_route('custom/v1', '/post-taxonomies/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_post_taxonomies',
'permission_callback' => '__return_true',
));
// 4. Endpunkt: Alle Medien-Dateien (Attachments) abrufen
register_rest_route('custom/v1', '/media', array(
'methods' => 'GET',
'callback' => 'rbr_get_all_media',
'permission_callback' => '__return_true',
'args' => array(
'per_page' => array(
'default' => -1,
'sanitize_callback' => 'rbr_sanitize_per_page',
),
),
));
// 5. Endpunkt: Medien-Datei direkt über ID abrufen
register_rest_route('custom/v1', '/media/(?P<id>\d+)', array(
'methods' => 'GET',
'callback' => 'rbr_get_media_by_id',
'permission_callback' => '__return_true',
));
});
/**
* Liefert ein Array mit allen veröffentlichten Post-IDs.
* Unterstützt verschiedene Post-Types (post, page, custom).
*/
function rbr_get_all_post_ids($request) {
$post_type = sanitize_text_field($request['post_type']);
$per_page = rbr_sanitize_per_page($request['per_page']);
$args = array(
'post_type' => $post_type,
'post_status' => 'publish',
'numberposts' => $per_page,
'fields' => 'ids',
'orderby' => 'date',
'order' => 'ASC',
);
$post_ids = get_posts($args);
if (empty($post_ids)) {
return new WP_Error('no_posts', 'Keine Beiträge gefunden.', array('status' => 404));
}
return rest_ensure_response(array(
'count' => count($post_ids),
'post_type' => $post_type,
'ids' => $post_ids,
));
}
/**
* Liefert Content, Featured Image, Kategorien, Keywords, Comments und alle im HTML/Post genutzten Medien-URLs.
*/
function rbr_get_post_details_with_media($data) {
$post_id = intval($data['id']);
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('no_post', 'Beitrag nicht gefunden.', array('status' => 404));
}
$content = apply_filters('the_content', $post->post_content);
$media_urls = array();
// 1. Featured Image (Titelbild) auslesen
$featured_image_url = '';
$featured_image_id = get_post_thumbnail_id($post_id);
if ($featured_image_id) {
$featured_image_url = wp_get_attachment_url($featured_image_id);
if ($featured_image_url) {
$media_urls[] = $featured_image_url;
}
}
// 2. Inhaltsbilder aus dem HTML extrahieren (via Regex für <img> Tags)
preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $post->post_content, $matches);
if (!empty($matches[1])) {
foreach ($matches[1] as $img_url) {
if (filter_var($img_url, FILTER_VALIDATE_URL)) {
$media_urls[] = $img_url;
}
}
}
// 3. Attachments die mit dem Post verknüpft sind
$attachments = get_posts(array(
'post_type' => 'attachment',
'numberposts' => -1,
'post_status' => null,
'post_parent' => $post_id,
'fields' => 'ids',
'post_mime_type' => 'image',
));
foreach ($attachments as $att_id) {
$att_url = wp_get_attachment_url($att_id);
if ($att_url) {
$media_urls[] = $att_url;
}
}
// Duplikate entfernen
$unique_media = array_values(array_unique($media_urls));
2026-09-22 08:08:51 +02:00
// 4. Kategorien
2026-09-20 08:31:23 +02:00
$categories = array();
$cats = get_the_category($post_id);
if (!empty($cats) && !is_wp_error($cats)) {
foreach ($cats as $cat) {
$categories[] = array(
'id' => intval($cat->term_id),
'name' => $cat->name,
'slug' => $cat->slug,
);
}
}
2026-09-22 08:08:51 +02:00
// 5. Tags (post_tag Taxonomy)
$tags = array();
$post_tags = get_the_tags($post_id);
if (!empty($post_tags) && !is_wp_error($post_tags)) {
foreach ($post_tags as $tag) {
$tags[] = array(
'id' => intval($tag->term_id),
'name' => $tag->name,
'slug' => $tag->slug,
);
}
}
// 6. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
2026-09-20 08:31:23 +02:00
$keywords = '';
2026-09-22 08:08:51 +02:00
// Yoast SEO: Prüfe auf Meta-Key direkt (funktioniert in allen Versionen)
$yoast_kw = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
if (!empty($yoast_kw)) {
$keywords = $yoast_kw;
2026-09-20 08:31:23 +02:00
}
// Rank Math
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
$rank_data = rank_math_get_single_post_data($post_id);
2026-09-22 08:08:51 +02:00
if (method_exists($rank_data, 'get_keywords')) {
$kw = $rank_data->get_keywords();
if (!empty($kw)) {
$keywords = $kw;
}
2026-09-20 08:31:23 +02:00
}
}
// Fallback: Custom Field 'post_keywords'
if (empty($keywords)) {
$keywords = get_post_meta($post_id, 'post_keywords', true);
}
2026-09-22 08:08:51 +02:00
// Fallback: Tags als Keywords
if (empty($keywords) && !empty($tags)) {
$keywords = implode(', ', array_column($tags, 'name'));
}
2026-09-20 08:31:23 +02:00
// Fallback: Kategorien als Keywords
if (empty($keywords) && !empty($categories)) {
$keywords = implode(', ', array_column($categories, 'name'));
}
// 6. Kommentare
$comments = array();
$wp_comments = get_comments(array(
'post_id' => $post_id,
'status' => 'approve',
'number' => 100,
));
foreach ($wp_comments as $comment) {
$comments[] = array(
'id' => intval($comment->comment_ID),
'author_name' => $comment->comment_author,
'author_email' => $comment->comment_author_email,
'author_url' => $comment->comment_author_url,
'content' => $comment->comment_content,
'date' => mysql_to_rfc3339($comment->comment_date),
);
}
$response = array(
'id' => intval($post->ID),
'title' => $post->post_title,
'slug' => $post->post_name,
'date' => mysql_to_rfc3339($post->post_date),
'content' => $content,
'excerpt' => wp_trim_words(strip_tags($post->post_excerpt), 55),
'author' => array(
'id' => intval($post->post_author),
'name' => get_the_author_meta('display_name', $post->post_author),
),
'featured_image' => $featured_image_url,
'categories' => $categories,
2026-09-22 08:08:51 +02:00
'tags' => $tags,
2026-09-20 08:31:23 +02:00
'keywords' => $keywords,
'comments' => $comments,
'media_files' => $unique_media,
);
return rest_ensure_response($response);
}
/**
* Liefert Kategorien und Tags für einen bestimmten Beitrag.
*/
function rbr_get_post_taxonomies($data) {
$post_id = intval($data['id']);
$post = get_post($post_id);
if (!$post || $post->post_status !== 'publish') {
return new WP_Error('no_post', 'Beitrag nicht gefunden.', array('status' => 404));
}
$taxonomies = get_object_taxonomies('post');
$terms = array();
foreach ($taxonomies as $taxonomy) {
$terms_list = get_the_terms($post_id, $taxonomy);
if (!empty($terms_list) && !is_wp_error($terms_list)) {
foreach ($terms_list as $term) {
$terms[] = array(
'id' => intval($term->term_id),
'name' => $term->name,
'slug' => $term->slug,
'type' => $taxonomy,
);
}
}
}
return rest_ensure_response(array(
'post_id' => $post_id,
'taxonomies' => $terms,
));
}
/**
* Liefert alle Medien-Dateien (Attachments) der WordPress-Instanz.
*/
function rbr_get_all_media($request) {
$per_page = rbr_sanitize_per_page($request['per_page']);
$args = array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'numberposts' => $per_page,
'orderby' => 'date',
'order' => 'ASC',
'post_mime_type' => array('image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'),
);
$attachments = get_posts($args);
$media_list = array();
foreach ($attachments as $att) {
$media_list[] = array(
'id' => intval($att->ID),
'url' => wp_get_attachment_url($att->ID),
'title' => $att->post_title,
'alt' => get_post_meta($att->ID, '_wp_attachment_image_alt', true),
'mime' => $att->post_mime_type,
'date' => mysql_to_rfc3339($att->post_date),
);
}
return rest_ensure_response(array(
'count' => count($media_list),
'media' => $media_list,
));
}
/**
* Liefert eine einzelne Medien-Datei anhand ihrer ID.
*/
function rbr_get_media_by_id($data) {
$media_id = intval($data['id']);
$attachment = get_post($media_id);
if (!$attachment || $attachment->post_type !== 'attachment') {
return new WP_Error('no_media', 'Medien-Datei nicht gefunden.', array('status' => 404));
}
return rest_ensure_response(array(
'id' => intval($attachment->ID),
'url' => wp_get_attachment_url($attachment->ID),
'title' => $attachment->post_title,
'alt' => get_post_meta($attachment->ID, '_wp_attachment_image_alt', true),
'mime' => $attachment->post_mime_type,
'date' => mysql_to_rfc3339($attachment->post_date),
));
}
/**
* Shortcode: [rbr_export] – Zeigt eine Export-Statistik im Frontend an.
*/
function rbr_export_shortcode($atts) {
$atts = shortcode_atts(array(
'post_type' => 'post',
), $atts, 'rbr_export');
$count = wp_count_posts($atts['post_type']);
$published = $count->publish ?? 0;
return sprintf(
'<div class="rbr-export-info"><p>Es gibt <strong>%d</strong> veröffentlichte %s-Beiträge, die über die REST-API exportiert werden können.</p></div>',
intval($published),
esc_html($atts['post_type'])
);
}
add_shortcode('rbr_export', 'rbr_export_shortcode');
/**
* Admin-Notiz: Plugin ist aktiv und bereit für den Export.
*/
add_action('admin_notices', function () {
if (current_user_can('manage_options')) {
echo '<div class="notice notice-info is-dismissible"><p>';
printf(
__('RBR Export to Ghost ist aktiv. <a href="%s">REST-API-Endpunkte</a> stehen unter <code>/wp-json/custom/v1/</code> zur Verfügung.', 'rbr-export-ghost'),
admin_url('tools.php')
);
echo '</p></div>';
}
});
/**
* CORS-Header erlauben (falls nötig für den C#-Importer)
*/
add_filter('rest_pre_serve_request', function ($served, $result, $request) {
if (strpos($request->get_route(), 'custom/v1/') === 0) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Authorization, Content-Type');
}
return $served;
}, 10, 3);
add_action('rest_api_init', function () {
// OPTIONS-Requests für CORS erlauben
register_rest_route('custom/v1', '/cors', array(
'methods' => 'OPTIONS',
'callback' => function () {
return new WP_REST_Response(null, 200);
},
'permission_callback' => '__return_true',
));
});