1328 lines
35 KiB
PHP
1328 lines
35 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Codex Blog Abilities
|
|
* Description: Exposes guarded WordPress administration abilities to the WordPress MCP Adapter.
|
|
* Version: 0.1.0
|
|
* Author: Codex
|
|
* License: GPL-2.0-or-later
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
* Requires at least: 6.9
|
|
* Requires PHP: 7.4
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
const CODEX_BLOG_ABILITIES_PLUGIN_FILE = 'codex-blog-abilities/codex-blog-abilities.php';
|
|
|
|
add_action(
|
|
'wp_abilities_api_categories_init',
|
|
static function() {
|
|
if ( ! function_exists( 'wp_register_ability_category' ) ) {
|
|
return;
|
|
}
|
|
|
|
wp_register_ability_category(
|
|
'codex-blog-admin',
|
|
array(
|
|
'label' => __( '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,
|
|
),
|
|
'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,
|
|
),
|
|
'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(
|
|
'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,
|
|
);
|
|
|
|
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_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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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_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_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 ) {
|
|
return array(
|
|
'id' => (int) $post->ID,
|
|
'title' => get_the_title( $post ),
|
|
'mime_type' => $post->post_mime_type,
|
|
'url' => wp_get_attachment_url( $post->ID ),
|
|
'date' => get_post_time( DATE_ATOM, false, $post ),
|
|
'alt_text' => get_post_meta( $post->ID, '_wp_attachment_image_alt', true ),
|
|
'parent_id' => (int) $post->post_parent,
|
|
);
|
|
},
|
|
$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 );
|
|
}
|