400 ) ); } if ( empty( $data['title'] ) || empty( $data['content'] ) || empty( $data['job_id'] ) ) { return new WP_Error( 'monroya_fields', 'title, content and job_id are required.', array( 'status' => 400 ) ); } $job_id = sanitize_text_field( $data['job_id'] ); // One draft per Monroya job: if it exists and is still unpublished, update it instead. $existing = self::find_by_job( $job_id ); if ( $existing ) { return self::update( $existing->ID, $data ); } $c = Monroya_Connection::get(); $post_id = wp_insert_post( array( 'post_type' => $type, 'post_status' => 'draft', 'post_title' => sanitize_text_field( $data['title'] ), 'post_content' => wp_kses_post( $data['content'] ), 'post_excerpt' => isset( $data['excerpt'] ) ? sanitize_textarea_field( $data['excerpt'] ) : '', 'post_name' => isset( $data['slug'] ) ? sanitize_title( $data['slug'] ) : '', 'post_author' => $c ? (int) $c['user_id'] : 0, 'post_parent' => 0, ), true ); if ( is_wp_error( $post_id ) ) { return $post_id; } update_post_meta( $post_id, self::META_JOB, $job_id ); self::apply_extras( $post_id, $type, $data ); self::notify_admin( $post_id ); return self::describe( $post_id ); } /** @return array|WP_Error */ public static function update( $post_id, array $data ) { $post = get_post( $post_id ); $err = self::assert_ours_and_unpublished( $post ); if ( is_wp_error( $err ) ) { return $err; } $update = array( 'ID' => $post->ID, 'post_status' => $post->post_status, // never changes status ); if ( isset( $data['title'] ) ) { $update['post_title'] = sanitize_text_field( $data['title'] ); } if ( isset( $data['content'] ) ) { $update['post_content'] = wp_kses_post( $data['content'] ); } if ( isset( $data['excerpt'] ) ) { $update['post_excerpt'] = sanitize_textarea_field( $data['excerpt'] ); } $result = wp_update_post( $update, true ); if ( is_wp_error( $result ) ) { return $result; } self::apply_extras( $post->ID, $post->post_type, $data ); return self::describe( $post->ID ); } /** @return array|WP_Error */ public static function get_status( $post_id ) { $post = get_post( $post_id ); if ( ! $post || ! get_post_meta( $post->ID, self::META_JOB, true ) ) { return new WP_Error( 'monroya_not_found', 'Draft not found.', array( 'status' => 404 ) ); } return self::describe( $post->ID ); } public static function find_by_job( $job_id ) { $posts = get_posts( array( 'post_type' => self::ALLOWED_TYPES, 'post_status' => array( 'draft', 'pending', 'future', 'publish', 'private' ), 'meta_key' => self::META_JOB, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- single lookup by job id. 'meta_value' => $job_id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- single lookup by job id. 'posts_per_page' => 1, ) ); return $posts ? $posts[0] : null; } /** @return true|WP_Error */ private static function assert_ours_and_unpublished( $post ) { if ( ! $post || ! in_array( $post->post_type, self::ALLOWED_TYPES, true ) || ! get_post_meta( $post->ID, self::META_JOB, true ) ) { return new WP_Error( 'monroya_not_ours', 'Monroya can only change drafts it created.', array( 'status' => 403 ) ); } if ( ! in_array( $post->post_status, array( 'draft', 'pending' ), true ) ) { return new WP_Error( 'monroya_published', 'This draft was already published or scheduled; Monroya will not change it.', array( 'status' => 409 ) ); } return true; } private static function apply_extras( $post_id, $type, array $data ) { // Categories and tags (posts only). Categories: existing ones only, never creates new categories. if ( 'post' === $type ) { if ( ! empty( $data['categories'] ) && is_array( $data['categories'] ) ) { $ids = array(); foreach ( $data['categories'] as $name ) { $term = term_exists( sanitize_text_field( $name ), 'category' ); if ( $term ) { $ids[] = (int) ( is_array( $term ) ? $term['term_id'] : $term ); } } if ( $ids ) { wp_set_post_categories( $post_id, $ids ); } } if ( ! empty( $data['tags'] ) && is_array( $data['tags'] ) ) { wp_set_post_tags( $post_id, array_map( 'sanitize_text_field', $data['tags'] ) ); } } // SEO title and description: into the SEO plugin if present, otherwise stored for our own output. $seo_title = isset( $data['seo']['title'] ) ? sanitize_text_field( $data['seo']['title'] ) : ''; $seo_desc = isset( $data['seo']['description'] ) ? sanitize_text_field( $data['seo']['description'] ) : ''; if ( $seo_title || $seo_desc ) { self::write_seo_meta( $post_id, $seo_title, $seo_desc ); } // Structured data (JSON-LD). Validated and re-encoded; printed only on this page. if ( ! empty( $data['schema'] ) ) { $decoded = is_string( $data['schema'] ) ? json_decode( $data['schema'], true ) : $data['schema']; if ( is_array( $decoded ) ) { update_post_meta( $post_id, self::META_SCHEMA, wp_json_encode( $decoded ) ); } } // Featured image from an https URL. if ( ! empty( $data['featured_image_url'] ) ) { $att = self::sideload_image( $data['featured_image_url'], $post_id, isset( $data['featured_image_alt'] ) ? $data['featured_image_alt'] : '' ); if ( $att ) { set_post_thumbnail( $post_id, $att ); } } } public static function seo_plugin() { if ( defined( 'WPSEO_VERSION' ) ) { return 'yoast'; } if ( defined( 'RANK_MATH_VERSION' ) ) { return 'rankmath'; } if ( defined( 'AIOSEO_VERSION' ) ) { return 'aioseo'; } return 'none'; } private static function write_seo_meta( $post_id, $title, $desc ) { switch ( self::seo_plugin() ) { case 'yoast': if ( $title ) { update_post_meta( $post_id, '_yoast_wpseo_title', $title ); } if ( $desc ) { update_post_meta( $post_id, '_yoast_wpseo_metadesc', $desc ); } break; case 'rankmath': if ( $title ) { update_post_meta( $post_id, 'rank_math_title', $title ); } if ( $desc ) { update_post_meta( $post_id, 'rank_math_description', $desc ); } break; default: // AIOSEO keeps its data in its own table; we store ours and print it only when no SEO plugin is active. if ( $title ) { update_post_meta( $post_id, self::META_TITLE, $title ); } if ( $desc ) { update_post_meta( $post_id, self::META_DESC, $desc ); } } } private static function sideload_image( $url, $post_id, $alt ) { $url = esc_url_raw( $url ); if ( 0 !== strpos( $url, 'https://' ) ) { return 0; } require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $att_id = media_sideload_image( $url, $post_id, null, 'id' ); if ( is_wp_error( $att_id ) ) { return 0; } if ( $alt ) { update_post_meta( $att_id, '_wp_attachment_image_alt', sanitize_text_field( $alt ) ); } return (int) $att_id; } private static function notify_admin( $post_id ) { $post = get_post( $post_id ); $to = get_option( 'admin_email' ); /* translators: %s: draft title */ $subject = sprintf( __( 'Monroya prepared a draft: %s', 'monroya-connect' ), $post->post_title ); $message = sprintf( /* translators: 1: post type, 2: title, 3: edit link */ __( "A new %1\$s draft is ready for your review:\n\n%2\$s\n\nReview and publish: %3\$s\n\nMonroya never publishes. Nothing goes live until you publish it.", 'monroya-connect' ), $post->post_type, $post->post_title, admin_url( 'post.php?post=' . $post_id . '&action=edit' ) ); wp_mail( $to, $subject, $message ); } public static function describe( $post_id ) { $post = get_post( $post_id ); return array( 'id' => $post->ID, 'type' => $post->post_type, 'status' => $post->post_status, 'title' => $post->post_title, 'job_id' => get_post_meta( $post->ID, self::META_JOB, true ), 'edit_url' => admin_url( 'post.php?post=' . $post->ID . '&action=edit' ), 'permalink' => 'publish' === $post->post_status ? get_permalink( $post ) : null, ); } /** Count of Monroya drafts awaiting review. */ public static function pending_count() { $q = new WP_Query( array( 'post_type' => self::ALLOWED_TYPES, 'post_status' => array( 'draft', 'pending' ), 'meta_key' => self::META_JOB, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- only Monroya's own drafts, max 50. 'fields' => 'ids', 'posts_per_page' => 50, 'no_found_rows' => false, ) ); return (int) $q->found_posts; } }