__( 'Codex Blog Admin', 'codex-blog-abilities' ), 'description' => __( 'Administrative blog operations exposed to trusted MCP clients.', 'codex-blog-abilities' ), ) ); } ); add_action( 'wp_abilities_api_init', static function() { if ( ! function_exists( 'wp_register_ability' ) ) { return; } codex_blog_register_ability( 'get-site-info', 'Get Site Info', 'Read basic WordPress site, theme, and current authenticated user information.', null, 'codex_blog_get_site_info', static function() { return current_user_can( 'read' ); }, true ); codex_blog_register_ability( 'list-post-types', 'List Post Types', 'List registered post types and their key capabilities.', codex_blog_schema( array( 'include_non_public' => array( 'type' => 'boolean', 'description' => 'Whether to include non-public post types.', ), ) ), 'codex_blog_list_post_types', static function() { return current_user_can( 'edit_posts' ); }, true ); codex_blog_register_ability( 'list-posts', 'List Posts', 'Query posts, pages, or custom post types with pagination and basic filters.', codex_blog_schema( array( 'post_type' => array( 'type' => 'string', 'description' => 'Post type slug. Defaults to post.', ), 'status' => array( 'type' => 'string', 'description' => 'Post status such as publish, draft, pending, private, trash, or any.', ), 'search' => array( 'type' => 'string', 'description' => 'Search phrase.', ), 'page' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Page number. Defaults to 1.', ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'description' => 'Results per page. Defaults to 10.', ), 'orderby' => array( 'type' => 'string', 'description' => 'Order by field. Common values: date, modified, title, ID, menu_order.', ), 'order' => array( 'type' => 'string', 'enum' => array( 'ASC', 'DESC', 'asc', 'desc' ), 'description' => 'Sort order.', ), ) ), 'codex_blog_list_posts', static function( $input ) { $args = codex_blog_input( $input ); $post_type = codex_blog_post_type_from_input( $args ); $type_obj = get_post_type_object( $post_type ); return $type_obj && current_user_can( $type_obj->cap->edit_posts ); }, true ); codex_blog_register_ability( 'get-post', 'Get Post', 'Read one post, page, attachment, or custom post type item by ID.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Post ID.', ), ), array( 'id' ) ), 'codex_blog_get_post', static function( $input ) { $args = codex_blog_input( $input ); $post = get_post( (int) $args['id'] ); return $post && current_user_can( 'edit_post', $post->ID ); }, true ); codex_blog_register_ability( 'create-post', 'Create Post', 'Create a post, page, or custom post type item. Publishing requires the relevant publish capability.', codex_blog_schema( array( 'post_type' => array( 'type' => 'string' ), 'title' => array( 'type' => 'string' ), 'content' => array( 'type' => 'string' ), 'excerpt' => array( 'type' => 'string' ), 'status' => array( 'type' => 'string', 'enum' => array( 'draft', 'pending', 'publish', 'private' ), ), 'slug' => array( 'type' => 'string' ), 'parent_id' => array( 'type' => 'integer', 'minimum' => 0, ), 'featured_media_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Attachment ID to set as the featured image.', ), 'terms' => array( 'type' => 'object', 'description' => 'Object keyed by taxonomy slug. Values are arrays of term IDs, slugs, or names.', 'additionalProperties' => array( 'type' => 'array', 'items' => array( 'type' => array( 'integer', 'string' ), ), ), ), ), array( 'title' ) ), 'codex_blog_create_post', 'codex_blog_can_create_post', false ); codex_blog_register_ability( 'update-post', 'Update Post', 'Update fields and taxonomy terms for an existing post, page, or custom post type item.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, ), 'title' => array( 'type' => 'string' ), 'content' => array( 'type' => 'string' ), 'excerpt' => array( 'type' => 'string' ), 'status' => array( 'type' => 'string', 'enum' => array( 'draft', 'pending', 'publish', 'private', 'trash' ), ), 'slug' => array( 'type' => 'string' ), 'parent_id' => array( 'type' => 'integer', 'minimum' => 0, ), 'featured_media_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Attachment ID to set as the featured image.', ), 'terms' => array( 'type' => 'object', 'additionalProperties' => array( 'type' => 'array', 'items' => array( 'type' => array( 'integer', 'string' ), ), ), ), ), array( 'id' ) ), 'codex_blog_update_post', 'codex_blog_can_update_post', false ); codex_blog_register_ability( 'delete-post', 'Delete Post', 'Trash or permanently delete a post, page, attachment, or custom post type item.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, ), 'force' => array( 'type' => 'boolean', 'description' => 'If true, permanently delete. Otherwise move to trash when possible.', ), ), array( 'id' ) ), 'codex_blog_delete_post', static function( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['id'] ) && current_user_can( 'delete_post', (int) $args['id'] ); }, false, true ); codex_blog_register_ability( 'schedule-post', 'Schedule Post', 'Schedule an existing post, page, or custom post type item for future publication using the site timezone by default.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, ), 'date' => array( 'type' => 'string', 'description' => 'Future date/time. ISO 8601 is recommended; date-only strings are interpreted in the provided timezone or site timezone.', ), 'timezone' => array( 'type' => 'string', 'description' => 'Optional timezone such as Europe/Berlin. Defaults to the site timezone.', ), ), array( 'id', 'date' ) ), 'codex_blog_schedule_post', 'codex_blog_can_schedule_post', false ); codex_blog_register_ability( 'upload-media-from-url', 'Upload Media From URL', 'Download a remote media file into the WordPress media library and optionally attach metadata.', codex_blog_schema( array( 'url' => array( 'type' => 'string', 'format' => 'uri', 'description' => 'Remote media URL to sideload.', ), 'post_id' => array( 'type' => 'integer', 'minimum' => 1, 'description' => 'Optional parent post ID.', ), 'title' => array( 'type' => 'string' ), 'alt_text' => array( 'type' => 'string' ), 'caption' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), ), array( 'url' ) ), 'codex_blog_upload_media_from_url', static function() { return current_user_can( 'upload_files' ); }, false ); codex_blog_register_ability( 'set-featured-image', 'Set Featured Image', 'Set an existing image attachment as the featured image for a post, page, or custom post type item.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'attachment_id' => array( 'type' => 'integer', 'minimum' => 1, ), ), array( 'post_id', 'attachment_id' ) ), 'codex_blog_set_featured_image', 'codex_blog_can_set_featured_image', false ); codex_blog_register_ability( 'set-featured-image-from-url', 'Set Featured Image From URL', 'Download a remote image, add it to the media library, and set it as the featured image for a post.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'url' => array( 'type' => 'string', 'format' => 'uri', ), 'title' => array( 'type' => 'string' ), 'alt_text' => array( 'type' => 'string' ), 'caption' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), ), array( 'post_id', 'url' ) ), 'codex_blog_set_featured_image_from_url', 'codex_blog_can_set_featured_image_from_url', false ); codex_blog_register_ability( 'remove-featured-image', 'Remove Featured Image', 'Remove the featured image from a post, page, or custom post type item.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), ), array( 'post_id' ) ), 'codex_blog_remove_featured_image', static function( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ); }, false ); codex_blog_register_ability( 'get-seo-meta', 'Get SEO Meta', 'Read SEO metadata for a post, including All in One SEO data when available and known post meta fallbacks.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), ), array( 'post_id' ) ), 'codex_blog_get_seo_meta', static function( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ); }, true ); codex_blog_register_ability( 'update-seo-meta', 'Update SEO Meta', 'Update SEO title, description, canonical URL, keywords, social metadata, and robots directives where supported.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'title' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), 'canonical_url' => array( 'type' => 'string', 'format' => 'uri', ), 'keywords' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), ), 'focus_keyphrase' => array( 'type' => 'string' ), 'og_title' => array( 'type' => 'string' ), 'og_description' => array( 'type' => 'string' ), 'og_image_url' => array( 'type' => 'string', 'format' => 'uri', ), 'twitter_title' => array( 'type' => 'string' ), 'twitter_description' => array( 'type' => 'string' ), 'twitter_image_url' => array( 'type' => 'string', 'format' => 'uri', ), 'robots' => array( 'type' => 'object', 'additionalProperties' => false, 'properties' => array( 'noindex' => array( 'type' => 'boolean' ), 'nofollow' => array( 'type' => 'boolean' ), ), ), ), array( 'post_id' ) ), 'codex_blog_update_seo_meta', static function( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ); }, false ); codex_blog_register_ability( 'audit-post-seo', 'Audit Post SEO', 'Run a deterministic SEO content audit for a post: metadata length, featured image, headings, links, terms, slug, and word count.', codex_blog_schema( array( 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), ), array( 'post_id' ) ), 'codex_blog_audit_post_seo', static function( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ); }, true ); codex_blog_register_ability( 'list-terms', 'List Terms', 'List taxonomy terms.', codex_blog_schema( array( 'taxonomy' => array( 'type' => 'string' ), 'hide_empty' => array( 'type' => 'boolean' ), 'search' => array( 'type' => 'string' ), 'page' => array( 'type' => 'integer', 'minimum' => 1, ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, ), ), array( 'taxonomy' ) ), 'codex_blog_list_terms', static function() { return current_user_can( 'edit_posts' ); }, true ); codex_blog_register_ability( 'create-term', 'Create Term', 'Create a taxonomy term.', codex_blog_schema( array( 'taxonomy' => array( 'type' => 'string' ), 'name' => array( 'type' => 'string' ), 'slug' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), 'parent_id' => array( 'type' => 'integer', 'minimum' => 0, ), ), array( 'taxonomy', 'name' ) ), 'codex_blog_create_term', 'codex_blog_can_manage_terms', false ); codex_blog_register_ability( 'update-term', 'Update Term', 'Update a taxonomy term.', codex_blog_schema( array( 'taxonomy' => array( 'type' => 'string' ), 'term_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'name' => array( 'type' => 'string' ), 'slug' => array( 'type' => 'string' ), 'description' => array( 'type' => 'string' ), 'parent_id' => array( 'type' => 'integer', 'minimum' => 0, ), ), array( 'taxonomy', 'term_id' ) ), 'codex_blog_update_term', 'codex_blog_can_manage_terms', false ); codex_blog_register_ability( 'delete-term', 'Delete Term', 'Delete a taxonomy term.', codex_blog_schema( array( 'taxonomy' => array( 'type' => 'string' ), 'term_id' => array( 'type' => 'integer', 'minimum' => 1, ), ), array( 'taxonomy', 'term_id' ) ), 'codex_blog_delete_term', 'codex_blog_can_manage_terms', false, true ); codex_blog_register_ability( 'list-comments', 'List Comments', 'List comments with moderation status filters.', codex_blog_schema( array( 'status' => array( 'type' => 'string', 'description' => 'Comment status: all, approve, hold, spam, trash.', ), 'post_id' => array( 'type' => 'integer', 'minimum' => 1, ), 'page' => array( 'type' => 'integer', 'minimum' => 1, ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, ), ) ), 'codex_blog_list_comments', static function() { return current_user_can( 'moderate_comments' ); }, true ); codex_blog_register_ability( 'update-comment', 'Update Comment', 'Update a comment moderation status or content.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, ), 'status' => array( 'type' => 'string', 'enum' => array( 'approve', 'hold', 'spam', 'trash' ), ), 'content' => array( 'type' => 'string' ), ), array( 'id' ) ), 'codex_blog_update_comment', static function() { return current_user_can( 'moderate_comments' ); }, false ); codex_blog_register_ability( 'delete-comment', 'Delete Comment', 'Trash or permanently delete a comment.', codex_blog_schema( array( 'id' => array( 'type' => 'integer', 'minimum' => 1, ), 'force' => array( 'type' => 'boolean' ), ), array( 'id' ) ), 'codex_blog_delete_comment', static function() { return current_user_can( 'moderate_comments' ); }, false, true ); codex_blog_register_ability( 'list-media', 'List Media', 'List media library attachments.', codex_blog_schema( array( 'search' => array( 'type' => 'string' ), 'mime_type' => array( 'type' => 'string' ), 'page' => array( 'type' => 'integer', 'minimum' => 1, ), 'per_page' => array( 'type' => 'integer', 'minimum' => 1, 'maximum' => 100, ), ) ), 'codex_blog_list_media', static function() { return current_user_can( 'upload_files' ); }, true ); codex_blog_register_ability( 'list-plugins', 'List Plugins', 'List installed plugins and activation status.', null, 'codex_blog_list_plugins', static function() { return current_user_can( 'activate_plugins' ); }, true ); codex_blog_register_ability( 'activate-plugin', 'Activate Plugin', 'Activate an installed plugin by plugin file path.', codex_blog_schema( array( 'plugin_file' => array( 'type' => 'string', 'description' => 'Plugin file path, for example akismet/akismet.php.', ), ), array( 'plugin_file' ) ), 'codex_blog_activate_plugin', static function() { return current_user_can( 'activate_plugins' ); }, false ); codex_blog_register_ability( 'deactivate-plugin', 'Deactivate Plugin', 'Deactivate an installed plugin by plugin file path. Blocks deactivating MCP-critical plugins unless allow_critical is true.', codex_blog_schema( array( 'plugin_file' => array( 'type' => 'string' ), 'allow_critical' => array( 'type' => 'boolean', 'description' => 'Allow deactivating mcp-adapter or codex-blog-abilities.', ), ), array( 'plugin_file' ) ), 'codex_blog_deactivate_plugin', static function() { return current_user_can( 'activate_plugins' ); }, false ); codex_blog_register_ability( 'get-options', 'Get Options', 'Read selected safe WordPress options. If names is omitted, reads a common admin allowlist.', codex_blog_schema( array( 'names' => array( 'type' => 'array', 'items' => array( 'type' => 'string' ), ), ) ), 'codex_blog_get_options', static function() { return current_user_can( 'manage_options' ); }, true ); codex_blog_register_ability( 'update-options', 'Update Options', 'Update selected safe WordPress options from a strict allowlist.', codex_blog_schema( array( 'updates' => array( 'type' => 'object', 'additionalProperties' => true, ), ), array( 'updates' ) ), 'codex_blog_update_options', static function() { return current_user_can( 'manage_options' ); }, false ); } ); function codex_blog_register_ability( $slug, $label, $description, $input_schema, $callback, $permission_callback, $readonly, $destructive = false ) { $args = array( 'label' => __( $label, 'codex-blog-abilities' ), 'description' => __( $description, 'codex-blog-abilities' ), 'category' => 'codex-blog-admin', 'output_schema' => codex_blog_output_schema(), 'execute_callback' => $callback, 'permission_callback' => $permission_callback, 'meta' => array( 'mcp' => array( 'public' => true, ), 'annotations' => array( 'readonly' => (bool) $readonly, 'destructive' => (bool) $destructive, 'idempotent' => false, ), ), ); if ( null !== $input_schema ) { $args['input_schema'] = $input_schema; } wp_register_ability( 'codex-blog/' . $slug, $args ); } function codex_blog_schema( $properties, $required = array() ) { return array( 'type' => 'object', 'properties' => $properties, 'required' => $required, 'additionalProperties' => false, ); } function codex_blog_output_schema() { return array( 'type' => 'object', 'additionalProperties' => true, ); } function codex_blog_input( $input ) { if ( is_object( $input ) ) { $input = get_object_vars( $input ); } if ( ! is_array( $input ) ) { return array(); } foreach ( $input as $key => $value ) { if ( is_object( $value ) ) { $input[ $key ] = codex_blog_input( $value ); } elseif ( is_array( $value ) ) { $input[ $key ] = codex_blog_deep_normalize( $value ); } } return $input; } function codex_blog_deep_normalize( $value ) { if ( is_object( $value ) ) { return codex_blog_input( $value ); } if ( ! is_array( $value ) ) { return $value; } foreach ( $value as $key => $child ) { $value[ $key ] = codex_blog_deep_normalize( $child ); } return $value; } function codex_blog_limit( $value, $default, $min, $max ) { $value = (int) $value; if ( $value < $min ) { $value = $default; } return min( $max, max( $min, $value ) ); } function codex_blog_post_type_from_input( $args ) { $post_type = isset( $args['post_type'] ) ? sanitize_key( $args['post_type'] ) : 'post'; return $post_type ? $post_type : 'post'; } function codex_blog_post_to_array( WP_Post $post, $include_content = false ) { $data = array( 'id' => (int) $post->ID, 'post_type' => $post->post_type, 'status' => $post->post_status, 'title' => get_the_title( $post ), 'slug' => $post->post_name, 'author_id' => (int) $post->post_author, 'date' => get_post_time( DATE_ATOM, false, $post ), 'modified' => get_post_modified_time( DATE_ATOM, false, $post ), 'link' => get_permalink( $post ), 'parent_id' => (int) $post->post_parent, 'comment_count' => (int) $post->comment_count, 'featured_image' => codex_blog_get_featured_image( $post->ID ), ); if ( $include_content ) { $data['content'] = $post->post_content; $data['excerpt'] = $post->post_excerpt; $data['terms'] = codex_blog_get_post_terms( $post->ID ); } return $data; } function codex_blog_get_post_terms( $post_id ) { $result = array(); $taxonomies = get_object_taxonomies( get_post_type( $post_id ) ); foreach ( $taxonomies as $taxonomy ) { $terms = get_the_terms( $post_id, $taxonomy ); if ( is_wp_error( $terms ) || empty( $terms ) ) { $result[ $taxonomy ] = array(); continue; } $result[ $taxonomy ] = array_map( static function( WP_Term $term ) { return array( 'id' => (int) $term->term_id, 'name' => $term->name, 'slug' => $term->slug, ); }, $terms ); } return $result; } function codex_blog_get_featured_image( $post_id ) { $attachment_id = get_post_thumbnail_id( $post_id ); if ( ! $attachment_id ) { return null; } return codex_blog_attachment_to_array( $attachment_id ); } function codex_blog_get_site_info() { $current_user = wp_get_current_user(); $theme = wp_get_theme(); return array( 'name' => get_bloginfo( 'name' ), 'description' => get_bloginfo( 'description' ), 'url' => site_url(), 'home' => home_url(), 'wp_version' => get_bloginfo( 'version' ), 'active_theme' => array( 'name' => $theme->get( 'Name' ), 'version' => $theme->get( 'Version' ), ), 'current_user' => array( 'id' => (int) $current_user->ID, 'login' => $current_user->user_login, 'display' => $current_user->display_name, 'roles' => $current_user->roles, 'capability' => array( 'edit_posts' => current_user_can( 'edit_posts' ), 'manage_options' => current_user_can( 'manage_options' ), 'activate_plugins' => current_user_can( 'activate_plugins' ), ), ), ); } function codex_blog_list_post_types( $input ) { $args = codex_blog_input( $input ); $include_non_public = ! empty( $args['include_non_public'] ); $post_types = get_post_types( array(), 'objects' ); $result = array(); foreach ( $post_types as $post_type => $type_obj ) { if ( ! $include_non_public && ! $type_obj->public ) { continue; } $result[] = array( 'name' => $post_type, 'label' => $type_obj->label, 'public' => (bool) $type_obj->public, 'show_ui' => (bool) $type_obj->show_ui, 'hierarchical' => (bool) $type_obj->hierarchical, 'capabilities' => array( 'edit_posts' => $type_obj->cap->edit_posts, 'publish_posts' => $type_obj->cap->publish_posts, 'delete_posts' => $type_obj->cap->delete_posts, ), ); } return array( 'post_types' => $result ); } function codex_blog_list_posts( $input ) { $args = codex_blog_input( $input ); $post_type = codex_blog_post_type_from_input( $args ); $status = isset( $args['status'] ) ? sanitize_key( $args['status'] ) : 'any'; $page = codex_blog_limit( $args['page'] ?? 1, 1, 1, 100000 ); $per_page = codex_blog_limit( $args['per_page'] ?? 10, 10, 1, 100 ); $query_args = array( 'post_type' => $post_type, 'post_status' => $status ? $status : 'any', 'paged' => $page, 'posts_per_page' => $per_page, 'orderby' => isset( $args['orderby'] ) ? sanitize_key( $args['orderby'] ) : 'date', 'order' => isset( $args['order'] ) && 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC', ); if ( ! empty( $args['search'] ) ) { $query_args['s'] = sanitize_text_field( $args['search'] ); } $query = new WP_Query( $query_args ); return array( 'page' => $page, 'per_page' => $per_page, 'total' => (int) $query->found_posts, 'total_pages' => (int) $query->max_num_pages, 'posts' => array_map( static function( WP_Post $post ) { return codex_blog_post_to_array( $post, false ); }, $query->posts ), ); } function codex_blog_get_post( $input ) { $args = codex_blog_input( $input ); $post = get_post( (int) $args['id'] ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } return array( 'post' => codex_blog_post_to_array( $post, true ) ); } function codex_blog_can_create_post( $input ) { $args = codex_blog_input( $input ); $post_type = codex_blog_post_type_from_input( $args ); $type_obj = get_post_type_object( $post_type ); if ( ! $type_obj || ! current_user_can( $type_obj->cap->edit_posts ) ) { return false; } $status = isset( $args['status'] ) ? sanitize_key( $args['status'] ) : 'draft'; if ( in_array( $status, array( 'publish', 'private' ), true ) && ! current_user_can( $type_obj->cap->publish_posts ) ) { return false; } return true; } function codex_blog_create_post( $input ) { $args = codex_blog_input( $input ); $post_type = codex_blog_post_type_from_input( $args ); $status = isset( $args['status'] ) ? sanitize_key( $args['status'] ) : 'draft'; $postarr = array( 'post_type' => $post_type, 'post_status' => $status, 'post_title' => sanitize_text_field( $args['title'] ?? '' ), 'post_content' => isset( $args['content'] ) ? wp_kses_post( $args['content'] ) : '', 'post_excerpt' => isset( $args['excerpt'] ) ? wp_kses_post( $args['excerpt'] ) : '', ); if ( isset( $args['slug'] ) ) { $postarr['post_name'] = sanitize_title( $args['slug'] ); } if ( isset( $args['parent_id'] ) ) { $postarr['post_parent'] = (int) $args['parent_id']; } $post_id = wp_insert_post( $postarr, true ); if ( is_wp_error( $post_id ) ) { return $post_id; } $terms_result = codex_blog_apply_terms( $post_id, $args['terms'] ?? array() ); if ( is_wp_error( $terms_result ) ) { return $terms_result; } if ( ! empty( $args['featured_media_id'] ) ) { $featured_result = codex_blog_set_featured_image_for_post( $post_id, (int) $args['featured_media_id'] ); if ( is_wp_error( $featured_result ) ) { return $featured_result; } } return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( $post_id ), true ), ); } function codex_blog_can_update_post( $input ) { $args = codex_blog_input( $input ); if ( empty( $args['id'] ) || ! current_user_can( 'edit_post', (int) $args['id'] ) ) { return false; } if ( ! empty( $args['status'] ) && in_array( sanitize_key( $args['status'] ), array( 'publish', 'private' ), true ) ) { $post = get_post( (int) $args['id'] ); $type_obj = $post ? get_post_type_object( $post->post_type ) : null; return $type_obj && current_user_can( $type_obj->cap->publish_posts ); } return true; } function codex_blog_update_post( $input ) { $args = codex_blog_input( $input ); $post = get_post( (int) $args['id'] ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } $postarr = array( 'ID' => $post->ID ); if ( array_key_exists( 'title', $args ) ) { $postarr['post_title'] = sanitize_text_field( $args['title'] ); } if ( array_key_exists( 'content', $args ) ) { $postarr['post_content'] = wp_kses_post( $args['content'] ); } if ( array_key_exists( 'excerpt', $args ) ) { $postarr['post_excerpt'] = wp_kses_post( $args['excerpt'] ); } if ( array_key_exists( 'status', $args ) ) { $postarr['post_status'] = sanitize_key( $args['status'] ); } if ( array_key_exists( 'slug', $args ) ) { $postarr['post_name'] = sanitize_title( $args['slug'] ); } if ( array_key_exists( 'parent_id', $args ) ) { $postarr['post_parent'] = (int) $args['parent_id']; } $result = wp_update_post( $postarr, true ); if ( is_wp_error( $result ) ) { return $result; } $terms_result = codex_blog_apply_terms( $post->ID, $args['terms'] ?? array() ); if ( is_wp_error( $terms_result ) ) { return $terms_result; } if ( ! empty( $args['featured_media_id'] ) ) { $featured_result = codex_blog_set_featured_image_for_post( $post->ID, (int) $args['featured_media_id'] ); if ( is_wp_error( $featured_result ) ) { return $featured_result; } } return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( $post->ID ), true ), ); } function codex_blog_delete_post( $input ) { $args = codex_blog_input( $input ); $id = (int) $args['id']; $force = ! empty( $args['force'] ); $post = get_post( $id ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } $result = $force ? wp_delete_post( $id, true ) : wp_trash_post( $id ); if ( ! $result ) { return new WP_Error( 'codex_blog_delete_failed', 'The post could not be deleted.' ); } return array( 'success' => true, 'id' => $id, 'previous_status' => $post->post_status, 'force' => $force, ); } function codex_blog_can_schedule_post( $input ) { $args = codex_blog_input( $input ); $post = ! empty( $args['id'] ) ? get_post( (int) $args['id'] ) : null; if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { return false; } $type_obj = get_post_type_object( $post->post_type ); return $type_obj && current_user_can( $type_obj->cap->publish_posts ); } function codex_blog_schedule_post( $input ) { $args = codex_blog_input( $input ); $post = get_post( (int) $args['id'] ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } $date = codex_blog_parse_datetime( $args['date'] ?? '', $args['timezone'] ?? '' ); if ( is_wp_error( $date ) ) { return $date; } $site_timezone = wp_timezone(); $now = new DateTimeImmutable( 'now', $site_timezone ); if ( $date <= $now ) { return new WP_Error( 'codex_blog_schedule_past_date', 'Scheduled date must be in the future.' ); } $local = $date->setTimezone( $site_timezone )->format( 'Y-m-d H:i:s' ); $gmt = $date->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ); $result = wp_update_post( array( 'ID' => $post->ID, 'post_status' => 'future', 'post_date' => $local, 'post_date_gmt' => $gmt, 'edit_date' => true, ), true ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( $post->ID ), true ), ); } function codex_blog_parse_datetime( $date, $timezone ) { $date = is_string( $date ) ? trim( $date ) : ''; if ( '' === $date ) { return new WP_Error( 'codex_blog_missing_date', 'A date value is required.' ); } try { $tz = $timezone ? new DateTimeZone( sanitize_text_field( $timezone ) ) : wp_timezone(); return new DateTimeImmutable( $date, $tz ); } catch ( Exception $e ) { return new WP_Error( 'codex_blog_invalid_date', 'Invalid date or timezone: ' . $e->getMessage() ); } } function codex_blog_can_set_featured_image( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && ! empty( $args['attachment_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ); } function codex_blog_can_set_featured_image_from_url( $input ) { $args = codex_blog_input( $input ); return ! empty( $args['post_id'] ) && current_user_can( 'edit_post', (int) $args['post_id'] ) && current_user_can( 'upload_files' ); } function codex_blog_upload_media_from_url( $input ) { $args = codex_blog_input( $input ); $attachment_id = codex_blog_sideload_attachment_from_url( $args ); if ( is_wp_error( $attachment_id ) ) { return $attachment_id; } return array( 'success' => true, 'attachment' => codex_blog_attachment_to_array( $attachment_id ), ); } function codex_blog_set_featured_image( $input ) { $args = codex_blog_input( $input ); $result = codex_blog_set_featured_image_for_post( (int) $args['post_id'], (int) $args['attachment_id'] ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( (int) $args['post_id'] ), true ), 'featured_image' => codex_blog_attachment_to_array( (int) $args['attachment_id'] ), ); } function codex_blog_set_featured_image_from_url( $input ) { $args = codex_blog_input( $input ); if ( empty( $args['post_id'] ) ) { return new WP_Error( 'codex_blog_missing_post', 'A post_id is required.' ); } $args['post_id'] = (int) $args['post_id']; $attachment_id = codex_blog_sideload_attachment_from_url( $args ); if ( is_wp_error( $attachment_id ) ) { return $attachment_id; } $result = codex_blog_set_featured_image_for_post( $args['post_id'], $attachment_id ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( $args['post_id'] ), true ), 'featured_image' => codex_blog_attachment_to_array( $attachment_id ), ); } function codex_blog_remove_featured_image( $input ) { $args = codex_blog_input( $input ); $post_id = (int) $args['post_id']; $old_id = get_post_thumbnail_id( $post_id ); delete_post_thumbnail( $post_id ); return array( 'success' => true, 'post' => codex_blog_post_to_array( get_post( $post_id ), true ), 'previous_featured_image_id' => $old_id ? (int) $old_id : null, ); } function codex_blog_sideload_attachment_from_url( $args ) { $url = isset( $args['url'] ) ? esc_url_raw( $args['url'] ) : ''; if ( ! $url || ( function_exists( 'wp_http_validate_url' ) && ! wp_http_validate_url( $url ) ) ) { return new WP_Error( 'codex_blog_invalid_media_url', 'A valid remote media URL is required.' ); } $post_id = ! empty( $args['post_id'] ) ? (int) $args['post_id'] : 0; if ( $post_id && ! current_user_can( 'edit_post', $post_id ) ) { return new WP_Error( 'codex_blog_forbidden_parent_post', 'Current user cannot edit the parent post.' ); } require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $tmp = download_url( $url, 30 ); if ( is_wp_error( $tmp ) ) { return $tmp; } $path = wp_parse_url( $url, PHP_URL_PATH ); $name = $path ? wp_basename( $path ) : 'remote-media'; $name = sanitize_file_name( $name ? $name : 'remote-media' ); $file = array( 'name' => $name, 'tmp_name' => $tmp, ); $attachment_id = media_handle_sideload( $file, $post_id, isset( $args['description'] ) ? sanitize_textarea_field( $args['description'] ) : null ); if ( is_wp_error( $attachment_id ) ) { @unlink( $tmp ); return $attachment_id; } $metadata_result = codex_blog_update_attachment_fields( $attachment_id, $args ); if ( is_wp_error( $metadata_result ) ) { return $metadata_result; } return $attachment_id; } function codex_blog_update_attachment_fields( $attachment_id, $args ) { $update = array( 'ID' => (int) $attachment_id ); if ( array_key_exists( 'title', $args ) ) { $update['post_title'] = sanitize_text_field( $args['title'] ); } if ( array_key_exists( 'caption', $args ) ) { $update['post_excerpt'] = sanitize_textarea_field( $args['caption'] ); } if ( array_key_exists( 'description', $args ) ) { $update['post_content'] = sanitize_textarea_field( $args['description'] ); } if ( count( $update ) > 1 ) { $result = wp_update_post( $update, true ); if ( is_wp_error( $result ) ) { return $result; } } if ( array_key_exists( 'alt_text', $args ) ) { update_post_meta( $attachment_id, '_wp_attachment_image_alt', sanitize_text_field( $args['alt_text'] ) ); } return true; } function codex_blog_attachment_to_array( $attachment_id ) { $attachment = get_post( (int) $attachment_id ); if ( ! $attachment || 'attachment' !== $attachment->post_type ) { return null; } $meta = wp_get_attachment_metadata( $attachment->ID ); return array( 'id' => (int) $attachment->ID, 'title' => get_the_title( $attachment ), 'mime_type' => $attachment->post_mime_type, 'url' => wp_get_attachment_url( $attachment->ID ), 'alt_text' => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ), 'caption' => $attachment->post_excerpt, 'description' => $attachment->post_content, 'parent_id' => (int) $attachment->post_parent, 'width' => isset( $meta['width'] ) ? (int) $meta['width'] : null, 'height' => isset( $meta['height'] ) ? (int) $meta['height'] : null, ); } function codex_blog_set_featured_image_for_post( $post_id, $attachment_id ) { $post = get_post( (int) $post_id ); $attachment = get_post( (int) $attachment_id ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } if ( ! $attachment || 'attachment' !== $attachment->post_type ) { return new WP_Error( 'codex_blog_attachment_not_found', 'Attachment not found.' ); } if ( ! wp_attachment_is_image( $attachment->ID ) ) { return new WP_Error( 'codex_blog_attachment_not_image', 'Featured image must be an image attachment.' ); } if ( ! current_user_can( 'edit_post', $post->ID ) ) { return new WP_Error( 'codex_blog_forbidden_post', 'Current user cannot edit this post.' ); } $result = set_post_thumbnail( $post->ID, $attachment->ID ); if ( ! $result && (int) get_post_thumbnail_id( $post->ID ) !== (int) $attachment->ID ) { return new WP_Error( 'codex_blog_featured_image_failed', 'Featured image could not be set.' ); } return true; } function codex_blog_apply_terms( $post_id, $terms_by_taxonomy ) { if ( empty( $terms_by_taxonomy ) ) { return true; } if ( ! is_array( $terms_by_taxonomy ) ) { return new WP_Error( 'codex_blog_invalid_terms', 'Terms must be an object keyed by taxonomy.' ); } foreach ( $terms_by_taxonomy as $taxonomy => $terms ) { $taxonomy = sanitize_key( $taxonomy ); if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'codex_blog_invalid_taxonomy', 'Invalid taxonomy: ' . $taxonomy ); } $tax_obj = get_taxonomy( $taxonomy ); if ( ! current_user_can( $tax_obj->cap->assign_terms ) ) { return new WP_Error( 'codex_blog_forbidden_terms', 'Current user cannot assign terms for taxonomy: ' . $taxonomy ); } $result = wp_set_object_terms( $post_id, $terms, $taxonomy, false ); if ( is_wp_error( $result ) ) { return $result; } } return true; } function codex_blog_get_seo_meta( $input ) { $args = codex_blog_input( $input ); $post_id = (int) $args['post_id']; if ( ! get_post( $post_id ) ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } return array( 'seo' => codex_blog_read_seo_meta( $post_id ), ); } function codex_blog_update_seo_meta( $input ) { $args = codex_blog_input( $input ); $post_id = (int) $args['post_id']; if ( ! get_post( $post_id ) ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } $updates = codex_blog_collect_seo_updates( $args ); if ( empty( $updates ) ) { return array( 'success' => true, 'updated' => false, 'seo' => codex_blog_read_seo_meta( $post_id ), ); } $aioseo_result = codex_blog_save_aioseo_meta( $post_id, $updates ); if ( is_wp_error( $aioseo_result ) ) { return $aioseo_result; } codex_blog_update_fallback_seo_meta( $post_id, $updates ); return array( 'success' => true, 'updated' => true, 'source' => $aioseo_result ? 'aioseo' : 'post_meta', 'seo' => codex_blog_read_seo_meta( $post_id ), ); } function codex_blog_audit_post_seo( $input ) { $args = codex_blog_input( $input ); $post_id = (int) $args['post_id']; $post = get_post( $post_id ); if ( ! $post ) { return new WP_Error( 'codex_blog_not_found', 'Post not found.' ); } $seo = codex_blog_read_seo_meta( $post_id ); $rendered = do_blocks( $post->post_content ); $text = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( strip_shortcodes( $rendered ) ) ) ); $title = ! empty( $seo['title'] ) ? $seo['title'] : get_the_title( $post ); $description = ! empty( $seo['description'] ) ? $seo['description'] : $post->post_excerpt; $link_counts = codex_blog_count_links( $rendered ); $h2_count = (int) preg_match_all( '/ 65 ) { $issues[] = 'SEO title length should usually be between 30 and 65 characters.'; } if ( empty( $seo['description'] ) ) { $issues[] = 'Meta description is missing.'; } elseif ( $desc_length < 70 || $desc_length > 160 ) { $issues[] = 'Meta description length should usually be between 70 and 160 characters.'; } if ( ! get_post_thumbnail_id( $post_id ) ) { $issues[] = 'Featured image is missing.'; } if ( $word_count < 300 ) { $issues[] = 'Content is shorter than 300 words.'; } if ( 0 === $h2_count ) { $issues[] = 'No H2 headings found.'; } if ( 0 === $link_counts['internal'] ) { $issues[] = 'No internal links found.'; } if ( empty( $terms['category'] ) ) { $issues[] = 'No category assigned.'; } if ( empty( $terms['post_tag'] ) ) { $issues[] = 'No tags assigned.'; } if ( strlen( $post->post_name ) > 75 ) { $issues[] = 'Slug is longer than 75 characters.'; } return array( 'post_id' => $post_id, 'metrics' => array( 'title_length' => $title_length, 'meta_description_length' => $desc_length, 'word_count' => $word_count, 'h2_count' => (int) $h2_count, 'internal_link_count' => $link_counts['internal'], 'external_link_count' => $link_counts['external'], 'has_featured_image' => (bool) get_post_thumbnail_id( $post_id ), 'has_custom_seo_title' => ! empty( $seo['title'] ), 'has_meta_description' => ! empty( $seo['description'] ), 'category_count' => empty( $terms['category'] ) ? 0 : count( $terms['category'] ), 'tag_count' => empty( $terms['post_tag'] ) ? 0 : count( $terms['post_tag'] ), 'slug_length' => strlen( $post->post_name ), ), 'issues' => $issues, 'seo' => $seo, ); } function codex_blog_read_seo_meta( $post_id ) { $model = codex_blog_get_aioseo_post( $post_id ); $seo = array( 'post_id' => (int) $post_id, 'source' => $model ? 'aioseo' : 'post_meta', 'title' => null, 'description' => null, 'canonical_url' => null, 'keywords' => null, 'focus_keyphrase' => null, 'og_title' => null, 'og_description' => null, 'og_image_url' => null, 'twitter_title' => null, 'twitter_description' => null, 'twitter_image_url' => null, 'robots' => array( 'noindex' => null, 'nofollow' => null, ), ); if ( $model ) { $seo['title'] = codex_blog_object_property( $model, 'title' ); $seo['description'] = codex_blog_object_property( $model, 'description' ); $seo['canonical_url'] = codex_blog_object_property( $model, 'canonical_url' ); $seo['keywords'] = codex_blog_object_property( $model, 'keywords' ); $seo['og_title'] = codex_blog_object_property( $model, 'og_title' ); $seo['og_description'] = codex_blog_object_property( $model, 'og_description' ); $seo['og_image_url'] = codex_blog_object_property( $model, 'og_image_url' ); $seo['twitter_title'] = codex_blog_object_property( $model, 'twitter_title' ); $seo['twitter_description'] = codex_blog_object_property( $model, 'twitter_description' ); $seo['twitter_image_url'] = codex_blog_object_property( $model, 'twitter_image_url' ); $seo['robots']['noindex'] = codex_blog_bool_from_mixed( codex_blog_object_property( $model, 'robots_noindex' ) ); $seo['robots']['nofollow'] = codex_blog_bool_from_mixed( codex_blog_object_property( $model, 'robots_nofollow' ) ); } $seo['title'] = codex_blog_value_or_meta( $seo['title'], $post_id, array( '_aioseo_title', '_aioseop_title', '_yoast_wpseo_title' ) ); $seo['description'] = codex_blog_value_or_meta( $seo['description'], $post_id, array( '_aioseo_description', '_aioseop_description', '_yoast_wpseo_metadesc' ) ); $seo['canonical_url'] = codex_blog_value_or_meta( $seo['canonical_url'], $post_id, array( '_aioseo_canonical_url', '_aioseop_custom_link', '_yoast_wpseo_canonical' ) ); $seo['keywords'] = codex_blog_value_or_meta( $seo['keywords'], $post_id, array( '_aioseo_keywords', '_aioseop_keywords', '_yoast_wpseo_metakeywords' ) ); $seo['focus_keyphrase'] = codex_blog_value_or_meta( $seo['focus_keyphrase'], $post_id, array( '_aioseo_focus_keyphrase', '_yoast_wpseo_focuskw' ) ); $seo['og_title'] = codex_blog_value_or_meta( $seo['og_title'], $post_id, array( '_aioseo_og_title', '_yoast_wpseo_opengraph-title' ) ); $seo['og_description'] = codex_blog_value_or_meta( $seo['og_description'], $post_id, array( '_aioseo_og_description', '_yoast_wpseo_opengraph-description' ) ); $seo['og_image_url'] = codex_blog_value_or_meta( $seo['og_image_url'], $post_id, array( '_aioseo_og_image_url', '_yoast_wpseo_opengraph-image' ) ); $seo['twitter_title'] = codex_blog_value_or_meta( $seo['twitter_title'], $post_id, array( '_aioseo_twitter_title', '_yoast_wpseo_twitter-title' ) ); $seo['twitter_description'] = codex_blog_value_or_meta( $seo['twitter_description'], $post_id, array( '_aioseo_twitter_description', '_yoast_wpseo_twitter-description' ) ); $seo['twitter_image_url'] = codex_blog_value_or_meta( $seo['twitter_image_url'], $post_id, array( '_aioseo_twitter_image_url', '_yoast_wpseo_twitter-image' ) ); if ( null === $seo['robots']['noindex'] ) { $seo['robots']['noindex'] = codex_blog_bool_from_mixed( codex_blog_first_post_meta( $post_id, array( '_aioseo_robots_noindex', '_aioseop_noindex', '_yoast_wpseo_meta-robots-noindex' ) ) ); } if ( null === $seo['robots']['nofollow'] ) { $seo['robots']['nofollow'] = codex_blog_bool_from_mixed( codex_blog_first_post_meta( $post_id, array( '_aioseo_robots_nofollow', '_aioseop_nofollow', '_yoast_wpseo_meta-robots-nofollow' ) ) ); } return $seo; } function codex_blog_get_aioseo_post( $post_id ) { if ( ! class_exists( '\\AIOSEO\\Plugin\\Common\\Models\\Post' ) ) { return null; } try { return \AIOSEO\Plugin\Common\Models\Post::getPost( $post_id ); } catch ( Throwable $e ) { return null; } } function codex_blog_object_property( $object, $property ) { if ( ! is_object( $object ) ) { return null; } try { if ( property_exists( $object, $property ) ) { return $object->$property; } if ( method_exists( $object, '__get' ) ) { $value = $object->$property; return null === $value ? null : $value; } return null; } catch ( Throwable $e ) { return null; } } function codex_blog_value_or_meta( $value, $post_id, $meta_keys ) { if ( null !== $value && '' !== $value && array() !== $value ) { return $value; } return codex_blog_first_post_meta( $post_id, $meta_keys ); } function codex_blog_first_post_meta( $post_id, $meta_keys ) { foreach ( $meta_keys as $meta_key ) { $value = get_post_meta( $post_id, $meta_key, true ); if ( null !== $value && '' !== $value && array() !== $value ) { return $value; } } return null; } function codex_blog_collect_seo_updates( $args ) { $updates = array(); $text_fields = array( 'title', 'focus_keyphrase', 'og_title', 'twitter_title', ); $textarea_fields = array( 'description', 'og_description', 'twitter_description', ); $url_fields = array( 'canonical_url', 'og_image_url', 'twitter_image_url', ); foreach ( $text_fields as $field ) { if ( array_key_exists( $field, $args ) ) { $updates[ $field ] = sanitize_text_field( $args[ $field ] ); } } foreach ( $textarea_fields as $field ) { if ( array_key_exists( $field, $args ) ) { $updates[ $field ] = sanitize_textarea_field( $args[ $field ] ); } } foreach ( $url_fields as $field ) { if ( array_key_exists( $field, $args ) ) { $updates[ $field ] = esc_url_raw( $args[ $field ] ); } } if ( array_key_exists( 'keywords', $args ) ) { $keywords = is_array( $args['keywords'] ) ? $args['keywords'] : array( $args['keywords'] ); $updates['keywords'] = implode( ', ', array_filter( array_map( 'sanitize_text_field', $keywords ) ) ); } if ( ! empty( $args['robots'] ) && is_array( $args['robots'] ) ) { $updates['robots'] = array(); if ( array_key_exists( 'noindex', $args['robots'] ) ) { $updates['robots']['noindex'] = (bool) $args['robots']['noindex']; } if ( array_key_exists( 'nofollow', $args['robots'] ) ) { $updates['robots']['nofollow'] = (bool) $args['robots']['nofollow']; } } return $updates; } function codex_blog_save_aioseo_meta( $post_id, $updates ) { if ( ! class_exists( '\\AIOSEO\\Plugin\\Common\\Models\\Post' ) ) { return false; } try { $model = \AIOSEO\Plugin\Common\Models\Post::getPost( $post_id ); if ( ! is_object( $model ) || ! method_exists( $model, 'save' ) ) { return false; } $model->post_id = (int) $post_id; $property_map = array( 'title' => 'title', 'description' => 'description', 'canonical_url' => 'canonical_url', 'og_title' => 'og_title', 'og_description' => 'og_description', 'twitter_title' => 'twitter_title', 'twitter_description' => 'twitter_description', ); foreach ( $property_map as $input_key => $property ) { if ( array_key_exists( $input_key, $updates ) ) { $model->$property = $updates[ $input_key ]; } } if ( array_key_exists( 'keywords', $updates ) ) { $model->keywords = codex_blog_aioseo_keywords_payload( $updates['keywords'] ); } if ( array_key_exists( 'focus_keyphrase', $updates ) ) { $model->keyphrases = codex_blog_aioseo_keyphrases_payload( $updates['focus_keyphrase'], $model ); } if ( array_key_exists( 'og_image_url', $updates ) ) { codex_blog_set_aioseo_image( $model, 'og', $updates['og_image_url'] ); } if ( array_key_exists( 'twitter_image_url', $updates ) ) { $model->twitter_use_og = false; codex_blog_set_aioseo_image( $model, 'twitter', $updates['twitter_image_url'] ); } if ( array_key_exists( 'twitter_title', $updates ) || array_key_exists( 'twitter_description', $updates ) ) { $model->twitter_use_og = false; } if ( ! empty( $updates['robots'] ) ) { $model->robots_default = false; if ( array_key_exists( 'noindex', $updates['robots'] ) ) { $model->robots_noindex = (bool) $updates['robots']['noindex']; } if ( array_key_exists( 'nofollow', $updates['robots'] ) ) { $model->robots_nofollow = (bool) $updates['robots']['nofollow']; } } $model->save(); codex_blog_bust_aioseo_post_cache( $post_id, $model ); do_action( 'aioseo_insert_post', $post_id ); return true; } catch ( Throwable $e ) { return new WP_Error( 'codex_blog_aioseo_save_failed', 'AIOSEO metadata could not be saved: ' . $e->getMessage() ); } } function codex_blog_aioseo_keywords_payload( $keywords ) { if ( ! is_string( $keywords ) ) { return $keywords; } $result = array(); foreach ( array_filter( array_map( 'trim', explode( ',', $keywords ) ) ) as $keyword ) { $result[] = array( 'value' => $keyword ); } return $result; } function codex_blog_aioseo_keyphrases_payload( $focus_keyphrase, $model ) { $existing = codex_blog_object_property( $model, 'keyphrases' ); $additional = array(); if ( is_array( $existing ) && ! empty( $existing['additional'] ) && is_array( $existing['additional'] ) ) { $additional = $existing['additional']; } elseif ( is_object( $existing ) && ! empty( $existing->additional ) && is_array( $existing->additional ) ) { $additional = $existing->additional; } return array( 'focus' => array( 'keyphrase' => $focus_keyphrase, 'score' => 0, 'analysis' => array(), ), 'additional' => $additional, ); } function codex_blog_set_aioseo_image( $model, $prefix, $url ) { $type_property = $prefix . '_image_type'; $url_property = $prefix . '_image_url'; $custom_property = $prefix . '_image_custom_url'; if ( '' === $url ) { $model->$type_property = 'default'; $model->$url_property = null; $model->$custom_property = null; return; } $model->$type_property = 'custom_image'; $model->$url_property = $url; $model->$custom_property = $url; } function codex_blog_bust_aioseo_post_cache( $post_id, $model ) { if ( ! function_exists( 'aioseo' ) ) { return; } $aioseo = aioseo(); if ( isset( $aioseo->meta ) && isset( $aioseo->meta->metaData ) && method_exists( $aioseo->meta->metaData, 'bustPostCache' ) ) { $aioseo->meta->metaData->bustPostCache( $post_id, $model ); } } function codex_blog_update_fallback_seo_meta( $post_id, $updates ) { $meta_map = array( 'title' => array( '_aioseo_title', '_aioseop_title', '_yoast_wpseo_title' ), 'description' => array( '_aioseo_description', '_aioseop_description', '_yoast_wpseo_metadesc' ), 'canonical_url' => array( '_aioseo_canonical_url', '_aioseop_custom_link', '_yoast_wpseo_canonical' ), 'keywords' => array( '_aioseo_keywords', '_aioseop_keywords', '_yoast_wpseo_metakeywords' ), 'focus_keyphrase' => array( '_aioseo_focus_keyphrase', '_yoast_wpseo_focuskw' ), 'og_title' => array( '_aioseo_og_title', '_yoast_wpseo_opengraph-title' ), 'og_description' => array( '_aioseo_og_description', '_yoast_wpseo_opengraph-description' ), 'og_image_url' => array( '_aioseo_og_image_url', '_yoast_wpseo_opengraph-image' ), 'twitter_title' => array( '_aioseo_twitter_title', '_yoast_wpseo_twitter-title' ), 'twitter_description' => array( '_aioseo_twitter_description', '_yoast_wpseo_twitter-description' ), 'twitter_image_url' => array( '_aioseo_twitter_image_url', '_yoast_wpseo_twitter-image' ), ); foreach ( $meta_map as $input_key => $meta_keys ) { if ( array_key_exists( $input_key, $updates ) ) { codex_blog_update_meta_aliases( $post_id, $meta_keys, $updates[ $input_key ] ); } } if ( ! empty( $updates['robots'] ) ) { if ( array_key_exists( 'noindex', $updates['robots'] ) ) { codex_blog_update_meta_aliases( $post_id, array( '_aioseo_robots_noindex', '_aioseop_noindex', '_yoast_wpseo_meta-robots-noindex' ), $updates['robots']['noindex'] ? '1' : '0' ); } if ( array_key_exists( 'nofollow', $updates['robots'] ) ) { codex_blog_update_meta_aliases( $post_id, array( '_aioseo_robots_nofollow', '_aioseop_nofollow', '_yoast_wpseo_meta-robots-nofollow' ), $updates['robots']['nofollow'] ? '1' : '0' ); } } } function codex_blog_update_meta_aliases( $post_id, $meta_keys, $value ) { update_post_meta( $post_id, $meta_keys[0], $value ); foreach ( array_slice( $meta_keys, 1 ) as $meta_key ) { if ( '' !== get_post_meta( $post_id, $meta_key, true ) ) { update_post_meta( $post_id, $meta_key, $value ); } } } function codex_blog_bool_from_mixed( $value ) { if ( null === $value || '' === $value ) { return null; } if ( is_bool( $value ) ) { return $value; } if ( is_numeric( $value ) ) { return (bool) (int) $value; } $value = strtolower( sanitize_text_field( (string) $value ) ); if ( in_array( $value, array( '1', 'yes', 'true', 'on' ), true ) ) { return true; } if ( in_array( $value, array( '0', 'no', 'false', 'off' ), true ) ) { return false; } return null; } function codex_blog_count_links( $html ) { $result = array( 'internal' => 0, 'external' => 0, ); if ( ! preg_match_all( '/]*href=[\"\']([^\"\']+)[\"\']/i', $html, $matches ) ) { return $result; } $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); foreach ( $matches[1] as $href ) { $href = trim( html_entity_decode( $href ) ); if ( '' === $href || '#' === $href[0] || 0 === strpos( $href, 'mailto:' ) || 0 === strpos( $href, 'tel:' ) ) { continue; } $host = wp_parse_url( $href, PHP_URL_HOST ); if ( ! $host || $host === $home_host ) { $result['internal']++; } else { $result['external']++; } } return $result; } function codex_blog_word_count( $text ) { $text = trim( wp_strip_all_tags( $text ) ); if ( '' === $text ) { return 0; } $words = preg_split( '/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY ); return $words ? count( $words ) : 0; } function codex_blog_list_terms( $input ) { $args = codex_blog_input( $input ); $taxonomy = sanitize_key( $args['taxonomy'] ); if ( ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'codex_blog_invalid_taxonomy', 'Invalid taxonomy.' ); } $page = codex_blog_limit( $args['page'] ?? 1, 1, 1, 100000 ); $per_page = codex_blog_limit( $args['per_page'] ?? 50, 50, 1, 100 ); $query = array( 'taxonomy' => $taxonomy, 'hide_empty' => ! empty( $args['hide_empty'] ), 'number' => $per_page, 'offset' => ( $page - 1 ) * $per_page, ); if ( ! empty( $args['search'] ) ) { $query['search'] = sanitize_text_field( $args['search'] ); } $terms = get_terms( $query ); if ( is_wp_error( $terms ) ) { return $terms; } return array( 'terms' => array_map( static function( WP_Term $term ) { return array( 'id' => (int) $term->term_id, 'taxonomy' => $term->taxonomy, 'name' => $term->name, 'slug' => $term->slug, 'description' => $term->description, 'parent_id' => (int) $term->parent, 'count' => (int) $term->count, ); }, $terms ), ); } function codex_blog_can_manage_terms( $input ) { $args = codex_blog_input( $input ); $taxonomy = isset( $args['taxonomy'] ) ? sanitize_key( $args['taxonomy'] ) : ''; $tax_obj = $taxonomy ? get_taxonomy( $taxonomy ) : null; return $tax_obj && current_user_can( $tax_obj->cap->manage_terms ); } function codex_blog_create_term( $input ) { $args = codex_blog_input( $input ); $taxonomy = sanitize_key( $args['taxonomy'] ); $termargs = array(); foreach ( array( 'slug', 'description' ) as $field ) { if ( isset( $args[ $field ] ) ) { $termargs[ $field ] = 'slug' === $field ? sanitize_title( $args[ $field ] ) : sanitize_text_field( $args[ $field ] ); } } if ( isset( $args['parent_id'] ) ) { $termargs['parent'] = (int) $args['parent_id']; } $result = wp_insert_term( sanitize_text_field( $args['name'] ), $taxonomy, $termargs ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'term_id' => (int) $result['term_id'], ); } function codex_blog_update_term( $input ) { $args = codex_blog_input( $input ); $taxonomy = sanitize_key( $args['taxonomy'] ); $termargs = array(); foreach ( array( 'name', 'slug', 'description' ) as $field ) { if ( isset( $args[ $field ] ) ) { $termargs[ $field ] = 'slug' === $field ? sanitize_title( $args[ $field ] ) : sanitize_text_field( $args[ $field ] ); } } if ( isset( $args['parent_id'] ) ) { $termargs['parent'] = (int) $args['parent_id']; } $result = wp_update_term( (int) $args['term_id'], $taxonomy, $termargs ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'term_id' => (int) $result['term_id'], ); } function codex_blog_delete_term( $input ) { $args = codex_blog_input( $input ); $result = wp_delete_term( (int) $args['term_id'], sanitize_key( $args['taxonomy'] ) ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => (bool) $result, 'deleted' => (bool) $result, ); } function codex_blog_list_comments( $input ) { $args = codex_blog_input( $input ); $page = codex_blog_limit( $args['page'] ?? 1, 1, 1, 100000 ); $per_page = codex_blog_limit( $args['per_page'] ?? 20, 20, 1, 100 ); $query = array( 'status' => isset( $args['status'] ) ? sanitize_key( $args['status'] ) : 'all', 'number' => $per_page, 'offset' => ( $page - 1 ) * $per_page, ); if ( ! empty( $args['post_id'] ) ) { $query['post_id'] = (int) $args['post_id']; } $comments = get_comments( $query ); return array( 'comments' => array_map( static function( WP_Comment $comment ) { return array( 'id' => (int) $comment->comment_ID, 'post_id' => (int) $comment->comment_post_ID, 'author' => $comment->comment_author, 'author_url' => $comment->comment_author_url, 'date' => mysql2date( DATE_ATOM, $comment->comment_date_gmt, false ), 'status' => wp_get_comment_status( $comment ), 'content' => $comment->comment_content, ); }, $comments ), ); } function codex_blog_update_comment( $input ) { $args = codex_blog_input( $input ); $id = (int) $args['id']; if ( isset( $args['status'] ) ) { wp_set_comment_status( $id, sanitize_key( $args['status'] ) ); } if ( isset( $args['content'] ) ) { $result = wp_update_comment( array( 'comment_ID' => $id, 'comment_content' => wp_kses_post( $args['content'] ), ) ); if ( false === $result ) { return new WP_Error( 'codex_blog_comment_update_failed', 'Comment update failed.' ); } } return array( 'success' => true, 'comment' => get_comment( $id, ARRAY_A ), ); } function codex_blog_delete_comment( $input ) { $args = codex_blog_input( $input ); $result = wp_delete_comment( (int) $args['id'], ! empty( $args['force'] ) ); return array( 'success' => (bool) $result, 'deleted' => (bool) $result, ); } function codex_blog_list_media( $input ) { $args = codex_blog_input( $input ); $page = codex_blog_limit( $args['page'] ?? 1, 1, 1, 100000 ); $per_page = codex_blog_limit( $args['per_page'] ?? 20, 20, 1, 100 ); $query_args = array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'paged' => $page, 'posts_per_page' => $per_page, ); if ( ! empty( $args['search'] ) ) { $query_args['s'] = sanitize_text_field( $args['search'] ); } if ( ! empty( $args['mime_type'] ) ) { $query_args['post_mime_type'] = sanitize_mime_type( $args['mime_type'] ); } $query = new WP_Query( $query_args ); return array( 'total' => (int) $query->found_posts, 'total_pages' => (int) $query->max_num_pages, 'media' => array_map( static function( WP_Post $post ) { $attachment = codex_blog_attachment_to_array( $post->ID ); $attachment['date'] = get_post_time( DATE_ATOM, false, $post ); return $attachment; }, $query->posts ), ); } function codex_blog_list_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); $result = array(); foreach ( $plugins as $plugin_file => $data ) { $result[] = array( 'plugin_file' => $plugin_file, 'name' => $data['Name'] ?? $plugin_file, 'version' => $data['Version'] ?? '', 'description' => wp_strip_all_tags( $data['Description'] ?? '' ), 'active' => is_plugin_active( $plugin_file ), ); } return array( 'plugins' => $result ); } function codex_blog_activate_plugin( $input ) { $args = codex_blog_input( $input ); $plugin_file = plugin_basename( sanitize_text_field( $args['plugin_file'] ) ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( ! array_key_exists( $plugin_file, get_plugins() ) ) { return new WP_Error( 'codex_blog_plugin_not_found', 'Plugin not found.' ); } $result = activate_plugin( $plugin_file ); if ( is_wp_error( $result ) ) { return $result; } return array( 'success' => true, 'plugin_file' => $plugin_file, 'active' => is_plugin_active( $plugin_file ), ); } function codex_blog_deactivate_plugin( $input ) { $args = codex_blog_input( $input ); $plugin_file = plugin_basename( sanitize_text_field( $args['plugin_file'] ) ); $critical = array( CODEX_BLOG_ABILITIES_PLUGIN_FILE, 'mcp-adapter/mcp-adapter.php', ); if ( ! function_exists( 'deactivate_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( in_array( $plugin_file, $critical, true ) && empty( $args['allow_critical'] ) ) { return new WP_Error( 'codex_blog_critical_plugin', 'Refusing to deactivate an MCP-critical plugin unless allow_critical is true.' ); } deactivate_plugins( $plugin_file ); return array( 'success' => true, 'plugin_file' => $plugin_file, 'active' => is_plugin_active( $plugin_file ), ); } function codex_blog_option_allowlist() { return array( 'blogname', 'blogdescription', 'admin_email', 'start_of_week', 'timezone_string', 'date_format', 'time_format', 'posts_per_page', 'default_category', 'default_post_format', 'default_comment_status', 'default_ping_status', 'comment_moderation', 'comment_previously_approved', 'permalink_structure', ); } function codex_blog_get_options( $input ) { $args = codex_blog_input( $input ); $allowlist = codex_blog_option_allowlist(); $names = ! empty( $args['names'] ) && is_array( $args['names'] ) ? $args['names'] : $allowlist; $result = array(); foreach ( $names as $name ) { $name = sanitize_key( $name ); if ( ! in_array( $name, $allowlist, true ) ) { continue; } $result[ $name ] = get_option( $name ); } return array( 'options' => $result, 'allowlist' => $allowlist, ); } function codex_blog_update_options( $input ) { $args = codex_blog_input( $input ); $updates = isset( $args['updates'] ) && is_array( $args['updates'] ) ? $args['updates'] : array(); $allowlist = codex_blog_option_allowlist(); $result = array(); foreach ( $updates as $name => $value ) { $name = sanitize_key( $name ); if ( ! in_array( $name, $allowlist, true ) ) { $result[ $name ] = array( 'updated' => false, 'error' => 'Option is not in the allowlist.', ); continue; } $old_value = get_option( $name ); $new_value = is_string( $value ) ? sanitize_text_field( $value ) : $value; $updated = update_option( $name, $new_value ); if ( 'permalink_structure' === $name ) { flush_rewrite_rules(); } $result[ $name ] = array( 'updated' => (bool) $updated, 'previous_value' => $old_value, 'new_value' => get_option( $name ), ); } return array( 'options' => $result ); }