396 lines
No EOL
12 KiB
PHP
396 lines
No EOL
12 KiB
PHP
<?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));
|
||
|
||
// 4. Kategorien ( Categories Only, keine Tags)
|
||
$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,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 5. Keywords (aus Yoast SEO oder Rank Math oder Custom Field)
|
||
$keywords = '';
|
||
// Yoast SEO
|
||
if (function_exists('yoast_get_primary_term')) {
|
||
$keywords = get_post_meta($post_id, '_yoast_wpseo_focuskw', true);
|
||
}
|
||
// Rank Math
|
||
if (empty($keywords) && function_exists('rank_math_get_single_post_data')) {
|
||
$rank_data = rank_math_get_single_post_data($post_id);
|
||
if (!empty($rank_data->get_keywords())) {
|
||
$keywords = $rank_data->get_keywords();
|
||
}
|
||
}
|
||
// Fallback: Custom Field 'post_keywords'
|
||
if (empty($keywords)) {
|
||
$keywords = get_post_meta($post_id, 'post_keywords', true);
|
||
}
|
||
// 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,
|
||
'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',
|
||
));
|
||
}); |