?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(); Restorative – Yoga Cards https://yoga-cards.com Yoga decks for your flow Thu, 14 May 2026 19:57:30 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://yoga-cards.com/wp-content/uploads/2025/10/logo-icon-yoga-cards-100x100.png Restorative – Yoga Cards https://yoga-cards.com 32 32 Child’s Pose / Balasana https://yoga-cards.com/yoga-pose/child-s-pose-balasana/ Thu, 14 May 2026 19:57:30 +0000 https://yoga-cards.com/yoga-pose/child-s-pose-balasana/ A simple yoga pose entry for Child’s Pose (Balasana).

]]>
Easy Seated / Sukhasana https://yoga-cards.com/yoga-pose/easy-seated-sukhasana-2/ Thu, 14 May 2026 19:47:29 +0000 https://yoga-cards.com/yoga-pose/easy-seated-sukhasana-2/ A simple yoga pose entry for Easy Seated (Sukhasana).

]]>
Easy Seated / Sukhasana https://yoga-cards.com/yoga-pose/easy-seated-sukhasana/ Wed, 13 May 2026 19:35:25 +0000 https://yoga-cards.com/?post_type=yoga-pose&p=110 A simple yoga pose entry for Easy Seated (Sukhasana).

]]>