?php
/**
* Plugin Name: Yoga Cards Structured REST Bridge
* Description: Provides a structured REST endpoint for creating or updating Yoga Cards pose entries with meta and taxonomies.
* Version: 1.0.0
* Author: Codex
* License: GPL-2.0-or-later
*
* @package YogaCardsStructuredRestBridge
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
final class YogaCards_Structured_Rest_Bridge {
const VERSION = '1.0.0';
const REST_NAMESPACE = 'yogacards/v1';
const REST_ROUTE = '/upsert-pose';
const DEFAULT_STATUS = 'draft';
const DEFAULT_POST_TYPE = 'yoga_pose';
/**
* Boot the plugin.
*
* @return void
*/
public static function boot() {
add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
}
/**
* Register REST routes.
*
* @return void
*/
public static function register_routes() {
register_rest_route(
self::REST_NAMESPACE,
self::REST_ROUTE,
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( __CLASS__, 'handle_upsert_pose' ),
'permission_callback' => array( __CLASS__, 'permissions_check' ),
)
);
}
/**
* Check whether the current user can create/update the target post type.
*
* @param WP_REST_Request $request Request object.
* @return bool
*/
public static function permissions_check( WP_REST_Request $request ) {
$post_type = self::resolve_post_type( self::get_param( $request, 'post_type', self::DEFAULT_POST_TYPE ) );
$post_type_object = get_post_type_object( $post_type );
if ( $post_type_object && isset( $post_type_object->cap->edit_posts ) ) {
return current_user_can( $post_type_object->cap->edit_posts );
}
return current_user_can( 'edit_posts' );
}
/**
* Handle create/update requests.
*
* Body shape:
* {
* "title": "Easy Seated / Sukhasana",
* "slug": "easy-seated-sukhasana",
* "status": "publish",
* "meta": {
* "pose_sanskrit": "Sukhasana",
* "overview": "..."
* },
* "taxonomies": {
* "phase": ["Warm-Up / Breath"],
* "focus": ["Breath"]
* },
* "featured_image_id": 123
* }
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response|WP_Error
*/
public static function handle_upsert_pose( WP_REST_Request $request ) {
$data = self::request_to_array( $request );
$title = self::get_string( $data, 'title', '' );
if ( '' === $title ) {
return new WP_Error( 'yogacards_missing_title', 'A title is required.', array( 'status' => 400 ) );
}
$post_type = self::resolve_post_type( self::get_string( $data, 'post_type', self::DEFAULT_POST_TYPE ) );
if ( ! post_type_exists( $post_type ) ) {
return new WP_Error(
'yogacards_unknown_post_type',
sprintf( 'The post type "%s" does not exist.', $post_type ),
array( 'status' => 400 )
);
}
$post_id = self::resolve_existing_post_id(
$post_type,
self::get_int( $data, 'id', 0 ),
self::get_string( $data, 'slug', '' )
);
$status = self::sanitize_post_status( self::get_string( $data, 'status', self::DEFAULT_STATUS ) );
$author_id = self::get_int( $data, 'author_id', get_current_user_id() );
$slug = self::get_string( $data, 'slug', '' );
$excerpt = self::get_string( $data, 'excerpt', '' );
$content = self::get_string( $data, 'content', '' );
$post_data = array(
'post_type' => $post_type,
'post_title' => $title,
'post_status' => $status,
'post_author' => $author_id > 0 ? $author_id : get_current_user_id(),
);
if ( '' !== $slug ) {
$post_data['post_name'] = sanitize_title( $slug );
}
if ( '' !== $excerpt ) {
$post_data['post_excerpt'] = $excerpt;
}
// Only store editor body content when it is explicitly provided.
if ( '' !== $content ) {
$post_data['post_content'] = $content;
}
if ( $post_id > 0 ) {
$post_data['ID'] = $post_id;
}
$inserted_id = wp_insert_post( wp_slash( $post_data ), true );
if ( is_wp_error( $inserted_id ) ) {
return $inserted_id;
}
$post_id = (int) $inserted_id;
self::apply_meta( $post_id, self::get_array( $data, 'meta', array() ) );
self::apply_taxonomies( $post_id, self::get_array( $data, 'taxonomies', array() ) );
self::apply_featured_image( $post_id, $data );
return rest_ensure_response(
array(
'ok' => true,
'mode' => $post_data['ID'] ?? 0 ? 'updated' : 'created',
'post_id' => $post_id,
'post_type' => get_post_type( $post_id ),
'post_status' => get_post_status( $post_id ),
'slug' => get_post_field( 'post_name', $post_id ),
'edit_url' => get_edit_post_link( $post_id, 'raw' ),
'view_url' => get_permalink( $post_id ),
'registered_meta' => array_keys( self::get_array( $data, 'meta', array() ) ),
)
);
}
/**
* Merge request params into one array, with JSON body taking precedence.
*
* @param WP_REST_Request $request Request object.
* @return array
*/
private static function request_to_array( WP_REST_Request $request ) {
$params = $request->get_params();
$json = $request->get_json_params();
if ( is_array( $json ) ) {
$params = array_merge( $params, $json );
}
return $params;
}
/**
* Resolve the post type, allowing either the request's value or the known Yoga Cards type.
*
* @param string $requested Requested post type.
* @return string
*/
private static function resolve_post_type( $requested ) {
$requested = sanitize_key( (string) $requested );
if ( '' !== $requested && post_type_exists( $requested ) ) {
return $requested;
}
$candidates = array(
'yoga_pose',
'yoga-pose',
);
foreach ( $candidates as $candidate ) {
if ( post_type_exists( $candidate ) ) {
return $candidate;
}
}
return self::DEFAULT_POST_TYPE;
}
/**
* Find an existing post by ID or slug.
*
* @param string $post_type Post type.
* @param int $id Optional explicit post ID.
* @param string $slug Optional slug.
* @return int
*/
private static function resolve_existing_post_id( $post_type, $id, $slug ) {
$id = absint( $id );
if ( $id > 0 && get_post( $id ) ) {
return $id;
}
$slug = sanitize_title( (string) $slug );
if ( '' === $slug ) {
return 0;
}
$existing = get_posts(
array(
'name' => $slug,
'post_type' => $post_type,
'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ),
'numberposts' => 1,
'fields' => 'ids',
'suppress_filters' => true,
)
);
if ( ! empty( $existing ) ) {
return (int) $existing[0];
}
return 0;
}
/**
* Apply meta fields to the post.
*
* @param int $post_id Post ID.
* @param array $meta Meta map.
* @return void
*/
private static function apply_meta( $post_id, array $meta ) {
foreach ( $meta as $key => $value ) {
$key = sanitize_key( (string) $key );
if ( '' === $key ) {
continue;
}
update_post_meta( $post_id, $key, $value );
}
}
/**
* Apply taxonomies using term IDs, names, or slugs.
*
* @param int $post_id Post ID.
* @param array $taxonomies Taxonomy map.
* @return void
*/
private static function apply_taxonomies( $post_id, array $taxonomies ) {
foreach ( $taxonomies as $taxonomy => $terms ) {
$taxonomy = sanitize_key( (string) $taxonomy );
if ( '' === $taxonomy || ! taxonomy_exists( $taxonomy ) ) {
continue;
}
if ( ! is_array( $terms ) ) {
$terms = array( $terms );
}
$term_ids = array();
foreach ( $terms as $term ) {
if ( is_int( $term ) || ctype_digit( (string) $term ) ) {
$term_ids[] = (int) $term;
continue;
}
$term = trim( (string) $term );
if ( '' === $term ) {
continue;
}
$existing = term_exists( $term, $taxonomy );
if ( is_array( $existing ) && isset( $existing['term_id'] ) ) {
$term_ids[] = (int) $existing['term_id'];
continue;
}
if ( is_int( $existing ) ) {
$term_ids[] = (int) $existing;
continue;
}
$inserted = wp_insert_term( $term, $taxonomy );
if ( ! is_wp_error( $inserted ) && isset( $inserted['term_id'] ) ) {
$term_ids[] = (int) $inserted['term_id'];
}
}
$term_ids = array_values( array_unique( array_filter( array_map( 'absint', $term_ids ) ) ) );
if ( ! empty( $term_ids ) ) {
wp_set_object_terms( $post_id, $term_ids, $taxonomy, false );
}
}
}
/**
* Apply featured image from an attachment ID or a remote URL.
*
* @param int $post_id Post ID.
* @param array $data Request data.
* @return void
*/
private static function apply_featured_image( $post_id, array $data ) {
$featured_image_id = self::get_int( $data, 'featured_image_id', 0 );
if ( $featured_image_id > 0 ) {
set_post_thumbnail( $post_id, $featured_image_id );
return;
}
$featured_image_url = self::get_string( $data, 'featured_image_url', '' );
if ( '' === $featured_image_url ) {
return;
}
$attachment_id = self::sideload_image_from_url( $featured_image_url, $post_id );
if ( $attachment_id > 0 ) {
set_post_thumbnail( $post_id, $attachment_id );
}
}
/**
* Sideload an image from a URL into the media library.
*
* @param string $url Remote image URL.
* @param int $post_id Parent post ID.
* @return int
*/
private static function sideload_image_from_url( $url, $post_id ) {
if ( ! function_exists( 'download_url' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! function_exists( 'media_handle_sideload' ) ) {
require_once ABSPATH . 'wp-admin/includes/media.php';
}
if ( ! function_exists( 'wp_generate_attachment_metadata' ) ) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$tmp = download_url( $url );
if ( is_wp_error( $tmp ) ) {
return 0;
}
$file_name = basename( wp_parse_url( $url, PHP_URL_PATH ) );
if ( '' === $file_name ) {
$file_name = 'yogacards-image.jpg';
}
$file_array = array(
'name' => $file_name,
'tmp_name' => $tmp,
);
$attachment_id = media_handle_sideload( $file_array, $post_id );
if ( is_wp_error( $attachment_id ) ) {
@unlink( $tmp );
return 0;
}
return (int) $attachment_id;
}
/**
* Get a string parameter from an array.
*
* @param array $data Data array.
* @param string $key Key name.
* @param string $default Default value.
* @return string
*/
private static function get_string( array $data, $key, $default = '' ) {
if ( ! array_key_exists( $key, $data ) ) {
return $default;
}
$value = $data[ $key ];
if ( is_array( $value ) || is_object( $value ) ) {
return $default;
}
return trim( (string) $value );
}
/**
* Get an integer parameter from an array.
*
* @param array $data Data array.
* @param string $key Key name.
* @param int $default Default value.
* @return int
*/
private static function get_int( array $data, $key, $default = 0 ) {
if ( ! array_key_exists( $key, $data ) ) {
return (int) $default;
}
return absint( $data[ $key ] );
}
/**
* Get an array parameter from an array.
*
* @param array $data Data array.
* @param string $key Key name.
* @param array $default Default value.
* @return array
*/
private static function get_array( array $data, $key, array $default = array() ) {
if ( ! array_key_exists( $key, $data ) ) {
return $default;
}
return is_array( $data[ $key ] ) ? $data[ $key ] : $default;
}
/**
* Normalize post status.
*
* @param string $status Post status.
* @return string
*/
private static function sanitize_post_status( $status ) {
$status = sanitize_key( (string) $status );
$allowed = array( 'draft', 'publish', 'pending', 'private', 'future' );
if ( ! in_array( $status, $allowed, true ) ) {
return self::DEFAULT_STATUS;
}
return $status;
}
}
YogaCards_Structured_Rest_Bridge::boot();