Initial commit: MCP Hub Community Edition v3.0.0

Community edition generated from private repo via sync pipeline.
Includes 9 plugins (WordPress, WooCommerce, WP Advanced, Gitea, n8n,
Supabase, OpenPanel, Appwrite, Directus) with ~587 tools.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
airano
2026-02-17 08:34:44 +03:30
commit cf62e65c55
237 changed files with 87596 additions and 0 deletions

View File

@@ -0,0 +1,375 @@
# WordPress Advanced Plugin
> **Advanced WordPress management features requiring elevated permissions**
## Overview
The WordPress Advanced plugin provides 22 powerful tools for advanced WordPress management, separated from the core WordPress plugin for better security and tool visibility.
### Why Separated?
**Phase D (WordPress Advanced Split)** separates advanced management features into their own plugin for:
1. **Better Security** 🔒
- Separate API keys for basic vs advanced operations
- Advanced operations require explicit permission
- Reduces risk of accidental database modifications
2. **Better Tool Visibility** 👁️
- Basic users see only 95 WordPress tools (not 117)
- Advanced users explicitly enable advanced features
- Cleaner tool list for most users
3. **Granular Access Control** 🎯
- Per-project API keys can grant access to:
- WordPress Core only (95 tools)
- WordPress Advanced only (22 tools)
- Both (117 tools total)
## Features
### 22 Advanced Tools
#### Database Operations (7 tools)
- `wp_db_export` - Export WordPress database to SQL file
- `wp_db_import` - Import SQL file to WordPress database
- `wp_db_size` - Get database size and table information
- `wp_db_tables` - List all database tables with sizes
- `wp_db_search` - Search database for specific content
- `wp_db_query` - Execute read-only SQL queries
- `wp_db_repair` - Repair and optimize database tables
#### Bulk Operations (8 tools)
- `bulk_update_posts` - Update multiple posts in parallel (max 100)
- `bulk_delete_posts` - Delete multiple posts in parallel (max 100)
- `bulk_update_products` - Update multiple products in parallel (max 100)
- `bulk_delete_products` - Delete multiple products in parallel (max 100)
- `bulk_delete_media` - Delete multiple media files in parallel (max 100)
- `bulk_assign_categories` - Assign categories to multiple posts
- `bulk_assign_tags` - Assign tags to multiple posts
- `bulk_trash_posts` - Move multiple posts to trash
#### System Operations (7 tools)
- `system_info` - Get WordPress system information (PHP, MySQL, server)
- `system_phpinfo` - Get detailed PHP configuration
- `system_disk_usage` - Get disk usage for WordPress installation
- `system_clear_all_caches` - Clear all WordPress caches
- `cron_list` - List all WordPress cron jobs
- `cron_run` - Run specific cron job immediately
- `error_log` - Get WordPress error log
## Requirements
### Core Requirements
- WordPress site with REST API enabled
- WordPress application password
- **Docker container name** (for WP-CLI access) - REQUIRED!
### WP-CLI Access
All WordPress Advanced features require WP-CLI access through Docker:
```bash
# The MCP server must have access to:
/var/run/docker.sock # Docker socket
# And the WordPress container must be accessible:
docker exec <container_name> wp --version
```
## Configuration
### Environment Variables
```bash
# WordPress Advanced Site 1
WORDPRESS_ADVANCED_SITE1_URL=https://example.com
WORDPRESS_ADVANCED_SITE1_USERNAME=admin
WORDPRESS_ADVANCED_SITE1_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
WORDPRESS_ADVANCED_SITE1_ALIAS=myblog # Optional: friendly name
WORDPRESS_ADVANCED_SITE1_CONTAINER=coolify-wp1 # REQUIRED: Docker container name
```
### Finding Container Name
```bash
# List all WordPress containers:
docker ps --filter "name=wordpress" --format "{{.Names}}"
# Test WP-CLI access:
docker exec <container_name> wp --info
```
## Usage Examples
### Database Operations
```python
# Export database
result = await mcp.call_tool("wordpress_advanced_wp_db_export", {
"site": "myblog",
"output_file": "/backup/db-backup.sql"
})
# Get database size
result = await mcp.call_tool("wordpress_advanced_wp_db_size", {
"site": "myblog"
})
# Search database
result = await mcp.call_tool("wordpress_advanced_wp_db_search", {
"site": "myblog",
"search_term": "old-domain.com",
"tables": ["wp_posts", "wp_options"]
})
# Execute read-only query
result = await mcp.call_tool("wordpress_advanced_wp_db_query", {
"site": "myblog",
"query": "SELECT COUNT(*) as total FROM wp_posts WHERE post_status='publish'"
})
```
### Bulk Operations
```python
# Bulk update posts
result = await mcp.call_tool("wordpress_advanced_bulk_update_posts", {
"site": "myblog",
"post_ids": [1, 2, 3, 4, 5],
"updates": {
"status": "draft",
"author": 2
}
})
# Bulk delete products
result = await mcp.call_tool("wordpress_advanced_bulk_delete_products", {
"site": "mystore",
"product_ids": [100, 101, 102],
"force": False # Move to trash instead of permanent delete
})
# Bulk assign categories
result = await mcp.call_tool("wordpress_advanced_bulk_assign_categories", {
"site": "myblog",
"post_ids": [10, 11, 12],
"category_ids": [5, 6]
})
```
### System Operations
```python
# Get system information
result = await mcp.call_tool("wordpress_advanced_system_info", {
"site": "myblog"
})
# Clear all caches
result = await mcp.call_tool("wordpress_advanced_system_clear_all_caches", {
"site": "myblog"
})
# List cron jobs
result = await mcp.call_tool("wordpress_advanced_cron_list", {
"site": "myblog"
})
# Get error log
result = await mcp.call_tool("wordpress_advanced_error_log", {
"site": "myblog",
"lines": 100
})
```
## Tool Count
```
WordPress Core Plugin: 95 tools ✅ (basic features)
WordPress Advanced Plugin: 22 tools 🔒 (advanced features)
─────────────────────────────────────────────────
Total (if both enabled): 117 tools
```
## API Key Configuration
### Option 1: WordPress Core Only (Basic Users)
```bash
# Create API key with wordpress scope only
# User gets: 95 WordPress tools
# User does NOT see: WordPress Advanced tools
```
### Option 2: WordPress Advanced Only (Power Users)
```bash
# Create API key with wordpress_advanced scope only
# User gets: 22 WordPress Advanced tools
# User does NOT see: WordPress Core tools
```
### Option 3: Both Plugins (Admin Users)
```bash
# Create API key with both scopes
# User gets: 117 total tools (95 + 22)
```
## Security Considerations
### Database Operations
- **wp_db_export**: Exports contain sensitive data - secure storage required
- **wp_db_import**: Can overwrite entire database - use with extreme caution
- **wp_db_query**: Read-only enforced - write queries are rejected
- **wp_db_search**: May expose sensitive information in results
### Bulk Operations
- **Parallel Execution**: Max 10 concurrent operations (controlled by semaphore)
- **Batch Limits**: Maximum 100 items per bulk operation
- **Error Handling**: Returns success/failure status for each item
- **Reversibility**: Most operations support trash (soft delete) before permanent deletion
### System Operations
- **system_clear_all_caches**: May cause temporary performance impact
- **cron_run**: Can trigger resource-intensive operations
- **error_log**: May contain sensitive information (paths, credentials)
## Troubleshooting
### "WP-CLI not available" Error
**Cause**: Container not configured or Docker socket not mounted
**Solution**:
```bash
# 1. Check container name
docker ps | grep wordpress
# 2. Test WP-CLI access
docker exec <container_name> wp --info
# 3. Verify Docker socket in docker-compose.yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
```
### "Database handler not available" Error
**Cause**: WP-CLI not configured (container name missing)
**Solution**:
```bash
# Ensure CONTAINER is set in environment variables
WORDPRESS_ADVANCED_SITE1_CONTAINER=your-container-name
```
### "Bulk operation failed" Error
**Cause**: Too many items or invalid IDs
**Solution**:
- Reduce batch size (max 100 items)
- Verify all IDs exist
- Check error details in response for specific failures
## Architecture
```
plugins/wordpress_advanced/
├── __init__.py # Plugin exports
├── plugin.py # WordPressAdvancedPlugin class
├── README.md # This file
├── schemas/ # Pydantic validation models
│ ├── __init__.py
│ ├── database.py # Database operation schemas
│ ├── bulk.py # Bulk operation schemas
│ └── system.py # System operation schemas
└── handlers/ # Tool implementations
├── __init__.py
├── database.py # Database operations (7 tools)
├── bulk.py # Bulk operations (8 tools)
└── system.py # System operations (7 tools)
```
## Migration from WordPress Core
If you previously used WordPress Phase 5 features (database, bulk, system operations):
### Before (Phase 5 - Single Plugin)
```bash
# All features in one plugin
WORDPRESS_SITE1_CONTAINER=coolify-wp1
# All 117 tools visible to everyone
```
### After (Phase D - Split Plugins)
```bash
# Basic WordPress (95 tools)
WORDPRESS_SITE1_URL=...
WORDPRESS_SITE1_USERNAME=...
WORDPRESS_SITE1_APP_PASSWORD=...
# Advanced WordPress (22 tools) - separate configuration
WORDPRESS_ADVANCED_SITE1_URL=...
WORDPRESS_ADVANCED_SITE1_USERNAME=...
WORDPRESS_ADVANCED_SITE1_APP_PASSWORD=...
WORDPRESS_ADVANCED_SITE1_CONTAINER=coolify-wp1 # REQUIRED
# API Keys can now control access separately
```
### Tool Name Changes
Tool names now include `wordpress_advanced_` prefix:
| Before (Phase 5) | After (Phase D) |
|-----------------------|---------------------------------------|
| `wp_db_export` | `wordpress_advanced_wp_db_export` |
| `bulk_update_posts` | `wordpress_advanced_bulk_update_posts`|
| `system_info` | `wordpress_advanced_system_info` |
## Performance
### Bulk Operations
- **Parallel Execution**: Up to 10 concurrent operations
- **Semaphore Control**: Prevents server overload
- **Progress Tracking**: Per-item success/failure status
- **Recommended Batch Size**: 10-50 items for optimal performance
### Database Operations
- **Export**: Time depends on database size (1GB ≈ 30-60 seconds)
- **Import**: Slightly slower than export due to indexing
- **Search**: Full-text search across specified tables
- **Query**: Fast read-only queries with result limits
### System Operations
- **Cache Clear**: 1-5 seconds depending on cache size
- **Cron Jobs**: Immediate execution, duration depends on job
- **System Info**: Near-instant (<1 second)
## Best Practices
1. **Use Separate API Keys**: Create different keys for basic and advanced operations
2. **Batch Size**: Keep bulk operations under 50 items for optimal performance
3. **Database Backups**: Always backup before using wp_db_import
4. **Cron Jobs**: Test cron jobs in staging environment first
5. **Error Logs**: Regularly check error logs for security issues
6. **Disk Usage**: Monitor disk usage before large export operations
## Support
For issues, feature requests, or contributions:
- GitHub Issues: [mcphub/issues](https://github.com/airano-ir/mcphub/issues)
- Documentation: [docs/](../../docs/)
- Main README: [../../README.md](../../README.md)
## License
Same as main project license.
---
**Part of MCP Hub** - Phase D (WordPress Advanced Split)
**Version**: 1.0.0
**Last Updated**: 2025-11-18

View File

@@ -0,0 +1,12 @@
"""
WordPress Advanced Plugin
Advanced WordPress management features including database operations,
bulk operations, and system management.
This plugin provides 22 advanced tools for WordPress power users and administrators.
"""
from .plugin import WordPressAdvancedPlugin
__all__ = ["WordPressAdvancedPlugin"]

View File

@@ -0,0 +1,26 @@
"""
WordPress Advanced Handlers
Modular handlers for WordPress advanced functionality.
Each handler is responsible for a specific domain of WordPress advanced operations.
"""
from plugins.wordpress_advanced.handlers.bulk import BulkHandler
from plugins.wordpress_advanced.handlers.bulk import get_tool_specifications as get_bulk_specs
from plugins.wordpress_advanced.handlers.database import DatabaseHandler
from plugins.wordpress_advanced.handlers.database import (
get_tool_specifications as get_database_specs,
)
from plugins.wordpress_advanced.handlers.system import SystemHandler
from plugins.wordpress_advanced.handlers.system import get_tool_specifications as get_system_specs
__all__ = [
# Handlers
"DatabaseHandler",
"BulkHandler",
"SystemHandler",
# Tool specifications
"get_database_specs",
"get_bulk_specs",
"get_system_specs",
]

View File

@@ -0,0 +1,687 @@
"""
Bulk Operations Handler
Manages WordPress bulk operations including:
- Bulk updates for posts/products/media
- Bulk deletions
- Bulk category/tag assignments
All operations use WordPress REST API batch requests for efficiency.
Operations are limited to 100 items per request for performance.
"""
import asyncio
from typing import Any
from plugins.wordpress.client import WordPressClient
from plugins.wordpress_advanced.schemas.bulk import (
BulkOperationResult,
)
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# Bulk Update Posts
{
"name": "bulk_update_posts",
"method_name": "bulk_update_posts",
"description": "Update multiple posts at once. Supports status, author, categories, tags, and more. Max 100 posts per request.",
"schema": {
"type": "object",
"properties": {
"post_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of post IDs to update",
},
"updates": {
"type": "object",
"description": "Fields to update (status, title, content, author, categories, tags, etc.)",
"properties": {
"status": {
"type": "string",
"enum": ["publish", "draft", "pending", "private"],
},
"title": {"type": "string"},
"content": {"type": "string"},
"excerpt": {"type": "string"},
"author": {"type": "integer"},
"categories": {"type": "array", "items": {"type": "integer"}},
"tags": {"type": "array", "items": {"type": "integer"}},
"featured_media": {"type": "integer"},
"comment_status": {"type": "string", "enum": ["open", "closed"]},
"ping_status": {"type": "string", "enum": ["open", "closed"]},
"sticky": {"type": "boolean"},
},
},
},
"required": ["post_ids", "updates"],
},
"scope": "write",
},
# Bulk Delete Posts
{
"name": "bulk_delete_posts",
"method_name": "bulk_delete_posts",
"description": "Delete multiple posts at once. Can move to trash or permanently delete. Max 100 posts per request.",
"schema": {
"type": "object",
"properties": {
"post_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of post IDs to delete",
},
"force": {
"type": "boolean",
"default": False,
"description": "Force permanent deletion (bypass trash)",
},
},
"required": ["post_ids"],
},
"scope": "admin",
},
# Bulk Update Products
{
"name": "bulk_update_products",
"method_name": "bulk_update_products",
"description": "Update multiple WooCommerce products at once. Supports price, stock, status, and more. Max 100 products per request.",
"schema": {
"type": "object",
"properties": {
"product_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of product IDs to update",
},
"updates": {
"type": "object",
"description": "Fields to update (price, stock, status, etc.)",
"properties": {
"name": {"type": "string"},
"status": {
"type": "string",
"enum": ["draft", "pending", "private", "publish"],
},
"featured": {"type": "boolean"},
"catalog_visibility": {
"type": "string",
"enum": ["visible", "catalog", "search", "hidden"],
},
"description": {"type": "string"},
"short_description": {"type": "string"},
"sku": {"type": "string"},
"price": {"type": "string"},
"regular_price": {"type": "string"},
"sale_price": {"type": "string"},
"stock_quantity": {"type": "integer"},
"stock_status": {
"type": "string",
"enum": ["instock", "outofstock", "onbackorder"],
},
"manage_stock": {"type": "boolean"},
"categories": {"type": "array", "items": {"type": "object"}},
"tags": {"type": "array", "items": {"type": "object"}},
},
},
},
"required": ["product_ids", "updates"],
},
"scope": "write",
},
# Bulk Delete Products
{
"name": "bulk_delete_products",
"method_name": "bulk_delete_products",
"description": "Delete multiple WooCommerce products at once. Permanently deletes products. Max 100 products per request.",
"schema": {
"type": "object",
"properties": {
"product_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of product IDs to delete",
},
"force": {
"type": "boolean",
"default": False,
"description": "Force permanent deletion",
},
},
"required": ["product_ids"],
},
"scope": "admin",
},
# Bulk Assign Categories
{
"name": "bulk_assign_categories",
"method_name": "bulk_assign_categories",
"description": "Assign categories to multiple posts/products at once. Can replace or append categories. Max 100 items per request. IMPORTANT: For posts use 'category' taxonomy IDs, for products use 'product_cat' taxonomy IDs.",
"schema": {
"type": "object",
"properties": {
"item_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of post/product IDs",
},
"category_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"description": "List of category IDs to assign. For posts: use 'category' taxonomy IDs. For products: use 'product_cat' taxonomy IDs.",
},
"replace": {
"type": "boolean",
"default": False,
"description": "Replace existing categories (true) or append (false)",
},
"item_type": {
"type": "string",
"enum": ["post", "product"],
"default": "post",
"description": "Type of items",
},
},
"required": ["item_ids", "category_ids"],
},
"scope": "write",
},
# Bulk Assign Tags
{
"name": "bulk_assign_tags",
"method_name": "bulk_assign_tags",
"description": "Assign tags to multiple posts/products at once. Can replace or append tags. Max 100 items per request. IMPORTANT: For posts use 'post_tag' taxonomy IDs, for products use 'product_tag' taxonomy IDs.",
"schema": {
"type": "object",
"properties": {
"item_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of post/product IDs",
},
"tag_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"description": "List of tag IDs to assign. For posts: use 'post_tag' taxonomy IDs. For products: use 'product_tag' taxonomy IDs.",
},
"replace": {
"type": "boolean",
"default": False,
"description": "Replace existing tags (true) or append (false)",
},
"item_type": {
"type": "string",
"enum": ["post", "product"],
"default": "post",
"description": "Type of items",
},
},
"required": ["item_ids", "tag_ids"],
},
"scope": "write",
},
# Bulk Update Media
{
"name": "bulk_update_media",
"method_name": "bulk_update_media",
"description": "Update multiple media items at once. Supports alt_text, title, caption, description. Max 100 items per request.",
"schema": {
"type": "object",
"properties": {
"media_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of media IDs to update",
},
"updates": {
"type": "object",
"description": "Fields to update",
"properties": {
"title": {"type": "string"},
"alt_text": {"type": "string"},
"caption": {"type": "string"},
"description": {"type": "string"},
},
},
},
"required": ["media_ids", "updates"],
},
"scope": "write",
},
# Bulk Delete Media
{
"name": "bulk_delete_media",
"method_name": "bulk_delete_media",
"description": "Delete multiple media items at once. Permanently deletes files from server. Max 100 items per request.",
"schema": {
"type": "object",
"properties": {
"media_ids": {
"type": "array",
"items": {"type": "integer"},
"minItems": 1,
"maxItems": 100,
"description": "List of media IDs to delete",
},
"force": {
"type": "boolean",
"default": True,
"description": "Force permanent deletion (media can't be trashed)",
},
},
"required": ["media_ids"],
},
"scope": "admin",
},
]
class BulkHandler:
"""Handles WordPress bulk operations"""
def __init__(self, client: WordPressClient):
"""
Initialize Bulk Handler
Args:
client: WordPress REST API client
"""
self.client = client
self.logger = client.logger
async def _bulk_operation(
self, endpoint: str, item_ids: list[int], operation: str, data: dict[str, Any] | None = None
) -> BulkOperationResult:
"""
Generic bulk operation executor
Args:
endpoint: REST API endpoint (e.g., 'posts', 'products')
item_ids: List of item IDs to process
operation: 'update' or 'delete'
data: Data for update operations
Returns:
BulkOperationResult with success/failure counts
"""
success_count = 0
failed_count = 0
failed_ids = []
errors = []
# Determine if using WooCommerce API
use_woocommerce = endpoint == "products"
# Determine HTTP method for updates
# WordPress REST API uses POST for media/posts updates, PUT for WooCommerce products
update_method = "PUT" if use_woocommerce else "POST"
# Process items in parallel (with limit to avoid overwhelming server)
semaphore = asyncio.Semaphore(10) # Max 10 concurrent requests
async def process_item(item_id: int):
nonlocal success_count, failed_count
async with semaphore:
try:
# Check if this is a WooCommerce endpoint
use_custom_namespace = endpoint.startswith("wc/")
if operation == "update":
await self.client.request(
update_method,
f"{endpoint}/{item_id}",
json_data=data,
use_custom_namespace=use_custom_namespace,
)
elif operation == "delete":
params = data or {}
await self.client.request(
"DELETE",
f"{endpoint}/{item_id}",
params=params,
use_custom_namespace=use_custom_namespace,
)
success_count += 1
return True
except Exception as e:
failed_count += 1
failed_ids.append(item_id)
errors.append({"id": item_id, "error": str(e)})
self.logger.error(f"Bulk {operation} failed for {endpoint}/{item_id}: {str(e)}")
return False
# Execute all operations in parallel
await asyncio.gather(*[process_item(item_id) for item_id in item_ids])
return {
"success_count": success_count,
"failed_count": failed_count,
"total": len(item_ids),
"failed_ids": failed_ids,
"errors": errors,
}
async def bulk_update_posts(
self, post_ids: list[int], updates: dict[str, Any]
) -> dict[str, Any]:
"""Bulk update posts"""
try:
result = await self._bulk_operation(
endpoint="posts", item_ids=post_ids, operation="update", data=updates
)
return {
"success": True,
"message": f"Updated {result['success_count']}/{result['total']} posts",
**result,
}
except Exception as e:
self.logger.error(f"Bulk update posts failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_delete_posts(self, post_ids: list[int], force: bool = False) -> dict[str, Any]:
"""Bulk delete posts"""
try:
result = await self._bulk_operation(
endpoint="posts", item_ids=post_ids, operation="delete", data={"force": force}
)
return {
"success": True,
"message": f"Deleted {result['success_count']}/{result['total']} posts",
"permanent": force,
**result,
}
except Exception as e:
self.logger.error(f"Bulk delete posts failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_update_products(
self, product_ids: list[int], updates: dict[str, Any]
) -> dict[str, Any]:
"""Bulk update WooCommerce products"""
try:
result = await self._bulk_operation(
endpoint="wc/v3/products", item_ids=product_ids, operation="update", data=updates
)
return {
"success": True,
"message": f"Updated {result['success_count']}/{result['total']} products",
**result,
}
except Exception as e:
self.logger.error(f"Bulk update products failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_delete_products(
self, product_ids: list[int], force: bool = False
) -> dict[str, Any]:
"""Bulk delete WooCommerce products"""
try:
result = await self._bulk_operation(
endpoint="wc/v3/products",
item_ids=product_ids,
operation="delete",
data={"force": force},
)
return {
"success": True,
"message": f"Deleted {result['success_count']}/{result['total']} products",
"permanent": force,
**result,
}
except Exception as e:
self.logger.error(f"Bulk delete products failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_assign_categories(
self,
item_ids: list[int],
category_ids: list[int],
replace: bool = False,
item_type: str = "post",
) -> dict[str, Any]:
"""
Bulk assign categories to posts/products.
IMPORTANT:
- For posts: use category IDs from 'category' taxonomy
- For products: use category IDs from 'product_cat' taxonomy
"""
try:
# Use correct endpoint for posts vs WooCommerce products
if item_type == "post":
endpoint = "posts"
elif item_type == "product":
endpoint = "wc/v3/products" # WooCommerce endpoint
else:
endpoint = "posts" # Default to posts
# Process each item individually to handle append mode
success_count = 0
failed_count = 0
failed_ids = []
errors = []
# Determine if using WooCommerce endpoint
use_custom_namespace = endpoint.startswith("wc/")
for item_id in item_ids:
try:
# If append mode, get current categories first
if not replace:
if use_custom_namespace:
current_item = await self.client.get(
f"{endpoint}/{item_id}", use_custom_namespace=True
)
current_categories = current_item.get("categories", [])
# Extract IDs from current categories
current_cat_ids = [
cat["id"] for cat in current_categories if "id" in cat
]
# Merge with new categories (avoid duplicates)
all_cat_ids = list(set(current_cat_ids + category_ids))
else:
current_item = await self.client.get(f"{endpoint}/{item_id}")
current_cat_ids = current_item.get("categories", [])
# Merge with new categories (avoid duplicates)
all_cat_ids = list(set(current_cat_ids + category_ids))
else:
# Replace mode: use only new categories
all_cat_ids = category_ids
# Format categories based on item type
if item_type == "product":
# WooCommerce requires categories as objects with id
updates = {"categories": [{"id": cat_id} for cat_id in all_cat_ids]}
else:
# WordPress posts use simple category ID array
updates = {"categories": all_cat_ids}
# Update the item
await self.client.request(
"PUT" if use_custom_namespace else "POST",
f"{endpoint}/{item_id}",
json_data=updates,
use_custom_namespace=use_custom_namespace,
)
success_count += 1
except Exception as e:
failed_count += 1
failed_ids.append(item_id)
errors.append({"id": item_id, "error": str(e)})
self.logger.error(
f"Failed to assign categories to {item_type} {item_id}: {str(e)}"
)
return {
"success": True,
"message": f"Assigned categories to {success_count}/{len(item_ids)} {item_type}s",
"mode": "replace" if replace else "append",
"success_count": success_count,
"failed_count": failed_count,
"total": len(item_ids),
"failed_ids": failed_ids,
"errors": errors,
}
except Exception as e:
self.logger.error(f"Bulk assign categories failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_assign_tags(
self,
item_ids: list[int],
tag_ids: list[int],
replace: bool = False,
item_type: str = "post",
) -> dict[str, Any]:
"""
Bulk assign tags to posts/products.
IMPORTANT:
- For posts: use tag IDs from 'post_tag' taxonomy
- For products: use tag IDs from 'product_tag' taxonomy
"""
try:
# Use correct endpoint for posts vs WooCommerce products
if item_type == "post":
endpoint = "posts"
elif item_type == "product":
endpoint = "wc/v3/products" # WooCommerce endpoint
else:
endpoint = "posts" # Default to posts
# Process each item individually to handle append mode
success_count = 0
failed_count = 0
failed_ids = []
errors = []
# Determine if using WooCommerce endpoint
use_custom_namespace = endpoint.startswith("wc/")
for item_id in item_ids:
try:
# If append mode, get current tags first
if not replace:
if use_custom_namespace:
current_item = await self.client.get(
f"{endpoint}/{item_id}", use_custom_namespace=True
)
current_tags = current_item.get("tags", [])
# Extract IDs from current tags
current_tag_ids = [tag["id"] for tag in current_tags if "id" in tag]
# Merge with new tags (avoid duplicates)
all_tag_ids = list(set(current_tag_ids + tag_ids))
else:
current_item = await self.client.get(f"{endpoint}/{item_id}")
current_tag_ids = current_item.get("tags", [])
# Merge with new tags (avoid duplicates)
all_tag_ids = list(set(current_tag_ids + tag_ids))
else:
# Replace mode: use only new tags
all_tag_ids = tag_ids
# Format tags based on item type
if item_type == "product":
# WooCommerce requires tags as objects with id
updates = {"tags": [{"id": tag_id} for tag_id in all_tag_ids]}
else:
# WordPress posts use simple tag ID array
updates = {"tags": all_tag_ids}
# Update the item
await self.client.request(
"PUT" if use_custom_namespace else "POST",
f"{endpoint}/{item_id}",
json_data=updates,
use_custom_namespace=use_custom_namespace,
)
success_count += 1
except Exception as e:
failed_count += 1
failed_ids.append(item_id)
errors.append({"id": item_id, "error": str(e)})
self.logger.error(f"Failed to assign tags to {item_type} {item_id}: {str(e)}")
return {
"success": True,
"message": f"Assigned tags to {success_count}/{len(item_ids)} {item_type}s",
"mode": "replace" if replace else "append",
"success_count": success_count,
"failed_count": failed_count,
"total": len(item_ids),
"failed_ids": failed_ids,
"errors": errors,
}
except Exception as e:
self.logger.error(f"Bulk assign tags failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_update_media(
self, media_ids: list[int], updates: dict[str, Any]
) -> dict[str, Any]:
"""Bulk update media items"""
try:
result = await self._bulk_operation(
endpoint="media", item_ids=media_ids, operation="update", data=updates
)
return {
"success": True,
"message": f"Updated {result['success_count']}/{result['total']} media items",
**result,
}
except Exception as e:
self.logger.error(f"Bulk update media failed: {str(e)}")
return {"success": False, "error": str(e)}
async def bulk_delete_media(self, media_ids: list[int], force: bool = True) -> dict[str, Any]:
"""Bulk delete media items"""
try:
result = await self._bulk_operation(
endpoint="media", item_ids=media_ids, operation="delete", data={"force": force}
)
return {
"success": True,
"message": f"Deleted {result['success_count']}/{result['total']} media items",
"permanent": force,
**result,
}
except Exception as e:
self.logger.error(f"Bulk delete media failed: {str(e)}")
return {"success": False, "error": str(e)}

View File

@@ -0,0 +1,589 @@
"""
Database Operations Handler
Manages WordPress database operations including:
- Export/Import
- Backup/Restore
- Optimization and Repair
- Search and Query operations
All operations require 'write' or 'admin' scope for security.
"""
import json
from typing import Any
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.wp_cli import WPCLIManager
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# DB Export (already exists in wp_cli.py, documented here for completeness)
{
"name": "wp_db_export",
"method_name": "wp_db_export",
"description": "Export WordPress database to SQL file. Creates a timestamped backup file in /tmp directory.",
"schema": {
"type": "object",
"properties": {
"tables": {
"type": "array",
"items": {"type": "string"},
"description": "Specific tables to export (default: all tables)",
},
"exclude_tables": {
"type": "array",
"items": {"type": "string"},
"description": "Tables to exclude from export",
},
"add_drop_table": {
"type": "boolean",
"default": True,
"description": "Include DROP TABLE statements",
},
},
},
"scope": "write",
},
# DB Import
{
"name": "wp_db_import",
"method_name": "wp_db_import",
"description": "Import database from SQL file. DESTRUCTIVE: replaces current database. Requires admin scope.",
"schema": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "Path to SQL file on server"},
"url": {"type": "string", "description": "URL to download SQL file from"},
"skip_optimization": {
"type": "boolean",
"default": False,
"description": "Skip database optimization after import",
},
},
},
"scope": "admin",
},
# DB Size Info
{
"name": "wp_db_size",
"method_name": "wp_db_size",
"description": "Get database size statistics including total size, table sizes, and row counts.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# DB Tables List
{
"name": "wp_db_tables",
"method_name": "wp_db_tables",
"description": "List all database tables with detailed information (size, engine, rows, collation).",
"schema": {
"type": "object",
"properties": {
"prefix_only": {
"type": "boolean",
"default": True,
"description": "Show only WordPress tables (with wp_ prefix)",
}
},
},
"scope": "read",
},
# DB Search
{
"name": "wp_db_search",
"method_name": "wp_db_search",
"description": "Search database for specific strings. Useful for finding content, debugging, or data migration.",
"schema": {
"type": "object",
"properties": {
"search_string": {"type": "string", "description": "String to search for"},
"tables": {
"type": "array",
"items": {"type": "string"},
"description": "Specific tables to search",
},
"regex": {
"type": "boolean",
"default": False,
"description": "Use regex pattern matching",
},
"case_sensitive": {
"type": "boolean",
"default": False,
"description": "Case-sensitive search",
},
"max_results": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000,
"description": "Maximum results to return",
},
},
"required": ["search_string"],
},
"scope": "read",
},
# DB Query (read-only SELECT)
{
"name": "wp_db_query",
"method_name": "wp_db_query",
"description": "Execute read-only SQL query (SELECT, SHOW, DESCRIBE only). For advanced users and debugging.",
"schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute (SELECT only)",
},
"max_rows": {
"type": "integer",
"default": 1000,
"minimum": 1,
"maximum": 10000,
"description": "Maximum rows to return",
},
},
"required": ["query"],
},
"scope": "write",
},
# DB Repair
{
"name": "wp_db_repair",
"method_name": "wp_db_repair",
"description": "Repair corrupted database tables. Runs REPAIR TABLE on all WordPress tables.",
"schema": {
"type": "object",
"properties": {
"tables": {
"type": "array",
"items": {"type": "string"},
"description": "Specific tables to repair (default: all tables)",
}
},
},
"scope": "write",
},
]
class DatabaseHandler:
"""Handles WordPress database operations"""
def __init__(self, client: WordPressClient, wp_cli: WPCLIManager | None = None):
"""
Initialize Database Handler
Args:
client: WordPress REST API client
wp_cli: WP-CLI manager (optional, for advanced operations)
"""
self.client = client
self.wp_cli = wp_cli
self.logger = client.logger
async def wp_db_export(
self,
tables: list[str] | None = None,
exclude_tables: list[str] | None = None,
add_drop_table: bool = True,
) -> dict[str, Any]:
"""
Export WordPress database to SQL file
Uses WP-CLI: wp db export
"""
if not self.wp_cli:
return {
"success": False,
"error": "WP-CLI not available. Container name not configured.",
}
try:
# Build WP-CLI command
cmd = "db export /tmp/backup-$(date +%Y%m%d-%H%M%S).sql"
if tables:
cmd += f" --tables={','.join(tables)}"
if exclude_tables:
cmd += f" --exclude_tables={','.join(exclude_tables)}"
if not add_drop_table:
cmd += " --no-add-drop-table"
# Execute export
result = await self.wp_cli.execute_command(cmd)
return {
"success": True,
"message": "Database exported successfully",
"file": result.get("output", ""),
"command": cmd,
}
except Exception as e:
self.logger.error(f"Database export failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_import(
self, file_path: str | None = None, url: str | None = None, skip_optimization: bool = False
) -> dict[str, Any]:
"""
Import database from SQL file
⚠️ DESTRUCTIVE OPERATION - Requires admin scope
"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
if not file_path and not url:
return {"success": False, "error": "Either file_path or url is required"}
try:
# Download file if URL provided
if url:
# Use wget or curl to download
download_cmd = f"wget -O /tmp/import.sql '{url}'"
await self.wp_cli.execute_command(f"eval '{download_cmd}'")
file_path = "/tmp/import.sql"
# Import database
cmd = f"db import {file_path}"
await self.wp_cli.execute_command(cmd)
# Optimize unless skipped
if not skip_optimization:
await self.wp_cli.execute_command("db optimize")
return {
"success": True,
"message": "Database imported successfully",
"file": file_path,
"optimized": not skip_optimization,
}
except Exception as e:
self.logger.error(f"Database import failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_size(self) -> dict[str, Any]:
"""Get database size statistics"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get total database size first
total_query = """
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS total_mb,
SUM(table_rows) AS total_rows,
COUNT(*) AS table_count
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
"""
total_cmd = f'db query "{total_query}" --skip-column-names'
total_result = await self.wp_cli.execute_command(total_cmd)
# Parse tab-separated output
total_output = total_result.get("output", "0\t0\t0").strip()
total_parts = total_output.split("\t")
total_size_mb = (
float(total_parts[0]) if len(total_parts) > 0 and total_parts[0] else 0.0
)
total_rows = int(total_parts[1]) if len(total_parts) > 1 and total_parts[1] else 0
table_count = int(total_parts[2]) if len(total_parts) > 2 and total_parts[2] else 0
# Get individual table sizes (top 50 largest tables)
tables_query = """
SELECT table_name,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS size_mb,
table_rows
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 50
"""
tables_cmd = f'db query "{tables_query}" --skip-column-names'
tables_result = await self.wp_cli.execute_command(tables_cmd)
# Parse table results (tab-separated)
tables_output = tables_result.get("output", "").strip()
tables = []
if tables_output:
for line in tables_output.split("\n"):
parts = line.split("\t")
if len(parts) >= 3:
tables.append(
{
"table_name": parts[0],
"size_mb": float(parts[1]) if parts[1] else 0.0,
"table_rows": int(parts[2]) if parts[2] else 0,
}
)
return {
"success": True,
"total_size_mb": total_size_mb,
"total_rows": total_rows,
"table_count": table_count,
"tables": tables,
"note": "Showing top 50 largest tables",
}
except Exception as e:
self.logger.error(f"Database size check failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_tables(self, prefix_only: bool = True) -> dict[str, Any]:
"""List all database tables with details"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Query for table information
query = """
SELECT
table_name,
engine,
table_rows,
ROUND((data_length / 1024 / 1024), 2),
ROUND((index_length / 1024 / 1024), 2),
ROUND(((data_length + index_length) / 1024 / 1024), 2),
table_collation
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
"""
if prefix_only:
# Get WordPress table prefix
prefix_result = await self.wp_cli.execute_command("config get table_prefix")
prefix = prefix_result.get("output", "wp_").strip()
query += f" AND table_name LIKE '{prefix}%'"
query += " ORDER BY (data_length + index_length) DESC"
# Use --skip-column-names for MariaDB compatibility (no --format=json)
cmd = f'db query "{query}" --skip-column-names'
result = await self.wp_cli.execute_command(cmd)
# Parse tab-separated output
tables_output = result.get("output", "").strip()
tables = []
if tables_output:
for line in tables_output.split("\n"):
parts = line.split("\t")
if len(parts) >= 7:
tables.append(
{
"name": parts[0],
"engine": parts[1],
"rows": int(parts[2]) if parts[2] and parts[2] != "NULL" else 0,
"data_size_mb": (
float(parts[3]) if parts[3] and parts[3] != "NULL" else 0.0
),
"index_size_mb": (
float(parts[4]) if parts[4] and parts[4] != "NULL" else 0.0
),
"total_size_mb": (
float(parts[5]) if parts[5] and parts[5] != "NULL" else 0.0
),
"collation": parts[6] if parts[6] != "NULL" else None,
}
)
return {"success": True, "tables": tables, "total": len(tables)}
except Exception as e:
self.logger.error(f"Database tables list failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_search(
self,
search_string: str,
tables: list[str] | None = None,
regex: bool = False,
case_sensitive: bool = False,
max_results: int = 100,
) -> dict[str, Any]:
"""Search database for specific strings"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Build search-replace command in dry-run mode
cmd = f'search-replace "{search_string}" "{search_string}" --dry-run --format=count'
if tables:
cmd += f" {' '.join(tables)}"
if regex:
cmd += " --regex"
if not case_sensitive:
cmd += " --skip-columns=guid" # Common practice
result = await self.wp_cli.execute_command(cmd)
return {
"success": True,
"search_string": search_string,
"matches_found": result.get("output", "0"),
"dry_run": True,
}
except Exception as e:
self.logger.error(f"Database search failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_query(self, query: str, max_rows: int = 1000) -> dict[str, Any]:
"""
Execute read-only SQL query
Security: Only SELECT, SHOW, DESCRIBE queries allowed
"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
# Validate query (additional server-side validation)
query_upper = query.strip().upper()
allowed_starts = ("SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN")
if not any(query_upper.startswith(cmd) for cmd in allowed_starts):
return {
"success": False,
"error": "Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries allowed",
}
forbidden = [
"INSERT",
"UPDATE",
"DELETE",
"DROP",
"ALTER",
"CREATE",
"TRUNCATE",
"REPLACE",
"GRANT",
"REVOKE",
]
for keyword in forbidden:
if keyword in query_upper:
return {"success": False, "error": f"Forbidden keyword in query: {keyword}"}
try:
# Add LIMIT if not present
if "LIMIT" not in query_upper:
query = f"{query.rstrip(';')} LIMIT {max_rows}"
# Execute query with --skip-column-names for MariaDB compatibility
# First, get column names separately if it's a SELECT
results = []
if query_upper.startswith("SELECT"):
# For SELECT queries, we need to parse the tab-separated output
cmd = f'db query "{query}" --skip-column-names'
result = await self.wp_cli.execute_command(cmd)
# Get output
output = result.get("output", "").strip()
if output:
# Parse tab-separated values
lines = output.split("\n")
# For simple queries, try to detect column structure
# We'll return as a list of dictionaries with column indices
for idx, line in enumerate(lines):
values = line.split("\t")
# Create a row dict with column indices
row = {f"col_{i}": val for i, val in enumerate(values)}
results.append(row)
# Limit results
if idx >= max_rows - 1:
break
else:
# For SHOW, DESCRIBE, etc., just return raw output
cmd = f'db query "{query}"'
result = await self.wp_cli.execute_command(cmd)
output = result.get("output", "").strip()
# Return as formatted message
return {
"success": True,
"output": output,
"query": query,
"note": "Results displayed as plain text",
}
return {
"success": True,
"results": results,
"row_count": len(results),
"query": query,
"note": "Columns are numbered as col_0, col_1, etc. due to MariaDB compatibility mode.",
}
except Exception as e:
self.logger.error(f"Database query failed: {str(e)}")
return {"success": False, "error": str(e)}
async def wp_db_repair(self, tables: list[str] | None = None) -> dict[str, Any]:
"""Repair corrupted database tables"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get list of tables to repair
if not tables:
# Get all WordPress tables
table_list = await self.wp_db_tables(prefix_only=True)
if not table_list.get("success"):
return table_list
tables = [t["name"] for t in table_list.get("tables", [])]
# Repair each table
results = []
for table in tables:
try:
query = f"REPAIR TABLE {table}"
cmd = f'db query "{query}" --format=json'
result = await self.wp_cli.execute_command(cmd)
repair_result = json.loads(result.get("output", "[]"))
results.append(
{
"table": table,
"status": "Repaired" if repair_result else "OK",
"message": str(repair_result),
}
)
except Exception as e:
results.append({"table": table, "status": "Failed", "message": str(e)})
# Count successes/failures
success_count = sum(1 for r in results if r["status"] != "Failed")
failed_count = len(results) - success_count
return {
"success": True,
"total_tables": len(results),
"success_count": success_count,
"failed_count": failed_count,
"results": results,
}
except Exception as e:
self.logger.error(f"Database repair failed: {str(e)}")
return {"success": False, "error": str(e)}

View File

@@ -0,0 +1,580 @@
"""
System Operations Handler
Manages WordPress system operations including:
- System information (PHP, MySQL, WordPress versions)
- Disk usage statistics
- Cron job management
- Cache operations
- Error log retrieval
Most operations require WP-CLI for advanced functionality.
"""
import json
import re
from typing import Any
from plugins.wordpress.client import WordPressClient
from plugins.wordpress.wp_cli import WPCLIManager
def get_tool_specifications() -> list[dict[str, Any]]:
"""Return tool specifications for ToolGenerator"""
return [
# System Info
{
"name": "system_info",
"method_name": "system_info",
"description": "Get comprehensive system information including PHP version, MySQL version, WordPress version, server info, memory limits, and more.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# PHP Info
{
"name": "system_phpinfo",
"method_name": "system_phpinfo",
"description": "Get detailed PHP configuration including loaded extensions, ini settings, and disabled functions.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# Disk Usage
{
"name": "system_disk_usage",
"method_name": "system_disk_usage",
"description": "Get disk usage statistics including uploads size, plugins size, themes size, and database size.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# Clear All Caches
{
"name": "system_clear_all_caches",
"method_name": "system_clear_all_caches",
"description": "Clear all caches including object cache, transients, and opcache (if available). Safe to run anytime.",
"schema": {"type": "object", "properties": {}},
"scope": "write",
},
# Cron List
{
"name": "cron_list",
"method_name": "cron_list",
"description": "List all scheduled WordPress cron jobs with schedule, next run time, and arguments.",
"schema": {"type": "object", "properties": {}},
"scope": "read",
},
# Cron Run
{
"name": "cron_run",
"method_name": "cron_run",
"description": "Manually trigger a specific cron job by hook name. Useful for testing or forcing scheduled tasks.",
"schema": {
"type": "object",
"properties": {
"hook": {"type": "string", "description": "Cron hook name to execute"},
"args": {
"type": "array",
"items": {},
"default": [],
"description": "Optional arguments to pass to the hook",
},
},
"required": ["hook"],
},
"scope": "write",
},
# Error Log
{
"name": "error_log",
"method_name": "error_log",
"description": "Get recent PHP error log entries. Useful for debugging issues. Admin scope recommended for security.",
"schema": {
"type": "object",
"properties": {
"lines": {
"type": "integer",
"default": 100,
"minimum": 1,
"maximum": 1000,
"description": "Number of log lines to retrieve",
},
"filter": {
"type": "string",
"description": "Filter logs by keyword (case-insensitive)",
},
"level": {
"type": "string",
"enum": ["error", "warning", "notice", "fatal"],
"description": "Filter by error level",
},
},
},
"scope": "read",
},
]
class SystemHandler:
"""Handles WordPress system operations"""
def __init__(self, client: WordPressClient, wp_cli: WPCLIManager | None = None):
"""
Initialize System Handler
Args:
client: WordPress REST API client
wp_cli: WP-CLI manager (optional, for advanced operations)
"""
self.client = client
self.wp_cli = wp_cli
self.logger = client.logger
async def wp_cli_version(self) -> dict[str, Any]:
"""Get WP-CLI version information"""
if not self.wp_cli:
return {"success": False, "version": None, "error": "WP-CLI not available"}
try:
result = await self.wp_cli.execute_command("cli version")
version_output = result.get("output", "").strip()
# Parse version (e.g., "WP-CLI 2.10.0")
version = (
version_output.replace("WP-CLI ", "").strip()
if "WP-CLI" in version_output
else version_output
)
return {"success": True, "version": version, "full_output": version_output}
except Exception as e:
self.logger.error(f"WP-CLI version check failed: {str(e)}")
return {"success": False, "version": None, "error": str(e)}
async def system_info(self) -> dict[str, Any]:
"""Get comprehensive system information"""
if not self.wp_cli:
return {
"success": False,
"error": "WP-CLI not available. Container name not configured.",
}
try:
# Get various system info using WP-CLI
info_commands = {
"php_version": "eval 'echo PHP_VERSION;'",
"wordpress_version": "core version",
"site_url": "option get siteurl",
"active_theme": "theme list --status=active --field=name",
"plugin_count": "plugin list --status=active --format=count",
"wp_debug": "config get WP_DEBUG",
"wp_debug_log": "config get WP_DEBUG_LOG",
"multisite": "config get MULTISITE",
}
results = {}
for key, cmd in info_commands.items():
try:
result = await self.wp_cli.execute_command(cmd)
results[key] = result.get("output", "").strip()
except Exception as e:
self.logger.warning(f"Failed to get {key}: {str(e)}")
results[key] = "N/A"
# Get PHP settings
php_settings_cmd = """eval 'echo json_encode([
"memory_limit" => ini_get("memory_limit"),
"max_execution_time" => ini_get("max_execution_time"),
"upload_max_filesize" => ini_get("upload_max_filesize"),
"post_max_size" => ini_get("post_max_size"),
"max_input_vars" => ini_get("max_input_vars")
]);'"""
php_result = await self.wp_cli.execute_command(php_settings_cmd)
php_settings = json.loads(php_result.get("output", "{}"))
# Get MySQL version
mysql_cmd = 'db query "SELECT VERSION()" --skip-column-names'
mysql_result = await self.wp_cli.execute_command(mysql_cmd)
mysql_version = mysql_result.get("output", "N/A").strip()
# Get server software from environment
server_cmd = 'eval \'echo $_SERVER["SERVER_SOFTWARE"] ?? "Unknown";\''
server_result = await self.wp_cli.execute_command(server_cmd)
server_software = server_result.get("output", "Unknown").strip()
# Get loaded PHP extensions
ext_cmd = "eval 'echo json_encode(get_loaded_extensions());'"
ext_result = await self.wp_cli.execute_command(ext_cmd)
php_extensions = json.loads(ext_result.get("output", "[]"))
return {
"success": True,
"php_version": results.get("php_version", "N/A"),
"mysql_version": mysql_version,
"wordpress_version": results.get("wordpress_version", "N/A"),
"server_software": server_software,
"memory_limit": php_settings.get("memory_limit", "N/A"),
"max_execution_time": int(php_settings.get("max_execution_time", 0)),
"upload_max_filesize": php_settings.get("upload_max_filesize", "N/A"),
"post_max_size": php_settings.get("post_max_size", "N/A"),
"max_input_vars": int(php_settings.get("max_input_vars", 0)),
"php_extensions": php_extensions,
"wp_debug": results.get("wp_debug", "false") == "true",
"wp_debug_log": results.get("wp_debug_log", "false") == "true",
"multisite": results.get("multisite", "false") == "true",
"active_plugins": int(results.get("plugin_count", 0)),
"active_theme": results.get("active_theme", "N/A"),
"site_url": results.get("site_url", "N/A"),
}
except Exception as e:
self.logger.error(f"System info failed: {str(e)}")
return {"success": False, "error": str(e)}
async def system_phpinfo(self) -> dict[str, Any]:
"""Get detailed PHP configuration"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get PHP version and SAPI
version_cmd = "eval 'echo PHP_VERSION;'"
version_result = await self.wp_cli.execute_command(version_cmd)
php_version = version_result.get("output", "").strip()
sapi_cmd = "eval 'echo PHP_SAPI;'"
sapi_result = await self.wp_cli.execute_command(sapi_cmd)
php_sapi = sapi_result.get("output", "").strip()
# Get loaded extensions
ext_cmd = "eval 'echo json_encode(get_loaded_extensions());'"
ext_result = await self.wp_cli.execute_command(ext_cmd)
extensions = json.loads(ext_result.get("output", "[]"))
# Get important ini settings
ini_settings_cmd = """eval 'echo json_encode([
"display_errors" => ini_get("display_errors"),
"error_reporting" => ini_get("error_reporting"),
"log_errors" => ini_get("log_errors"),
"error_log" => ini_get("error_log"),
"memory_limit" => ini_get("memory_limit"),
"max_execution_time" => ini_get("max_execution_time"),
"upload_max_filesize" => ini_get("upload_max_filesize"),
"post_max_size" => ini_get("post_max_size"),
"max_input_vars" => ini_get("max_input_vars"),
"max_input_time" => ini_get("max_input_time"),
"default_socket_timeout" => ini_get("default_socket_timeout"),
"allow_url_fopen" => ini_get("allow_url_fopen"),
"session.save_handler" => ini_get("session.save_handler")
]);'"""
ini_result = await self.wp_cli.execute_command(ini_settings_cmd)
ini_settings = json.loads(ini_result.get("output", "{}"))
# Get disabled functions
disabled_cmd = "eval 'echo ini_get(\"disable_functions\");'"
disabled_result = await self.wp_cli.execute_command(disabled_cmd)
disabled_raw = disabled_result.get("output", "").strip()
disabled_functions = [f.strip() for f in disabled_raw.split(",") if f.strip()]
return {
"success": True,
"version": php_version,
"sapi": php_sapi,
"extensions": extensions,
"ini_settings": ini_settings,
"disabled_functions": disabled_functions,
}
except Exception as e:
self.logger.error(f"PHP info failed: {str(e)}")
return {"success": False, "error": str(e)}
async def system_disk_usage(self) -> dict[str, Any]:
"""Get disk usage statistics"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get WordPress root path
path_cmd = "eval 'echo ABSPATH;'"
path_result = await self.wp_cli.execute_command(path_cmd)
wp_path = path_result.get("output", "").strip()
# Get directory sizes using du command
sizes = {}
# Uploads directory
uploads_cmd = f"eval 'echo shell_exec(\"du -sm {wp_path}wp-content/uploads 2>/dev/null | cut -f1\");'"
try:
uploads_result = await self.wp_cli.execute_command(uploads_cmd)
upload_size = uploads_result.get("output", "0").strip()
sizes["uploads_size_mb"] = (
float(upload_size) if upload_size and upload_size != "" else 0.0
)
except Exception as e:
self.logger.warning(f"Failed to get uploads size: {e}")
sizes["uploads_size_mb"] = 0.0
# Plugins directory
plugins_cmd = f"eval 'echo shell_exec(\"du -sm {wp_path}wp-content/plugins 2>/dev/null | cut -f1\");'"
try:
plugins_result = await self.wp_cli.execute_command(plugins_cmd)
plugins_size = plugins_result.get("output", "0").strip()
sizes["plugins_size_mb"] = (
float(plugins_size) if plugins_size and plugins_size != "" else 0.0
)
except Exception as e:
self.logger.warning(f"Failed to get plugins size: {e}")
sizes["plugins_size_mb"] = 0.0
# Themes directory
themes_cmd = f"eval 'echo shell_exec(\"du -sm {wp_path}wp-content/themes 2>/dev/null | cut -f1\");'"
try:
themes_result = await self.wp_cli.execute_command(themes_cmd)
themes_size = themes_result.get("output", "0").strip()
sizes["themes_size_mb"] = (
float(themes_size) if themes_size and themes_size != "" else 0.0
)
except Exception as e:
self.logger.warning(f"Failed to get themes size: {e}")
sizes["themes_size_mb"] = 0.0
# Total WordPress directory
total_cmd = f"eval 'echo shell_exec(\"du -sm {wp_path} 2>/dev/null | cut -f1\");'"
try:
total_result = await self.wp_cli.execute_command(total_cmd)
total_size = total_result.get("output", "0").strip()
sizes["wordpress_size_mb"] = (
float(total_size) if total_size and total_size != "" else 0.0
)
except Exception as e:
self.logger.warning(f"Failed to get wordpress total size: {e}")
sizes["wordpress_size_mb"] = 0.0
# Database size (from our database handler logic)
db_query = """
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2) AS size_mb
FROM information_schema.TABLES
WHERE table_schema = DATABASE()
"""
db_cmd = f'db query "{db_query}" --skip-column-names'
try:
db_result = await self.wp_cli.execute_command(db_cmd)
sizes["database_size_mb"] = float(db_result.get("output", "0").strip())
except:
sizes["database_size_mb"] = 0.0
# Calculate total
total_size = sum([sizes["wordpress_size_mb"], sizes["database_size_mb"]])
return {
"success": True,
"total_size_mb": round(total_size, 2),
**sizes,
"breakdown": {
"uploads": sizes["uploads_size_mb"],
"plugins": sizes["plugins_size_mb"],
"themes": sizes["themes_size_mb"],
"database": sizes["database_size_mb"],
},
}
except Exception as e:
self.logger.error(f"Disk usage check failed: {str(e)}")
return {"success": False, "error": str(e)}
async def system_clear_all_caches(self) -> dict[str, Any]:
"""Clear all caches"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
cleared = []
# Clear object cache
try:
await self.wp_cli.execute_command("cache flush")
cleared.append("object_cache")
except Exception as e:
self.logger.warning(f"Object cache flush failed: {str(e)}")
# Clear transients
try:
await self.wp_cli.execute_command("transient delete --all")
cleared.append("transients")
except Exception as e:
self.logger.warning(f"Transient delete failed: {str(e)}")
# Clear OPcache if available
try:
opcache_cmd = 'eval \'if (function_exists("opcache_reset")) { opcache_reset(); echo "cleared"; }\''
opcache_result = await self.wp_cli.execute_command(opcache_cmd)
if "cleared" in opcache_result.get("output", ""):
cleared.append("opcache")
except Exception as e:
self.logger.warning(f"OPcache clear failed: {str(e)}")
return {
"success": True,
"message": f"Cleared {len(cleared)} cache types",
"cleared": cleared,
}
except Exception as e:
self.logger.error(f"Clear all caches failed: {str(e)}")
return {"success": False, "error": str(e)}
async def cron_list(self) -> dict[str, Any]:
"""List all WordPress cron jobs"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get cron events
cmd = "cron event list --format=json"
result = await self.wp_cli.execute_command(cmd)
events_data = json.loads(result.get("output", "[]"))
# Parse events
events = []
for event in events_data:
events.append(
{
"hook": event.get("hook", ""),
"timestamp": int(event.get("time", 0)),
"schedule": event.get("recurrence", "single"),
"interval": event.get("interval"),
"args": event.get("args", []),
}
)
# Get available schedules
schedules_cmd = "cron schedule list --format=json"
try:
schedules_result = await self.wp_cli.execute_command(schedules_cmd)
schedules_data = json.loads(schedules_result.get("output", "[]"))
schedules = {s.get("name"): s for s in schedules_data}
except:
schedules = {}
return {"success": True, "events": events, "total": len(events), "schedules": schedules}
except Exception as e:
self.logger.error(f"Cron list failed: {str(e)}")
return {"success": False, "error": str(e)}
async def cron_run(self, hook: str, args: list[Any] | None = None) -> dict[str, Any]:
"""Manually run a cron job"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Build command
cmd = f"cron event run {hook}"
# Execute cron job
result = await self.wp_cli.execute_command(cmd)
return {
"success": True,
"message": f"Cron job '{hook}' executed",
"hook": hook,
"output": result.get("output", ""),
}
except Exception as e:
self.logger.error(f"Cron run failed: {str(e)}")
return {"success": False, "error": str(e)}
async def error_log(
self, lines: int = 100, filter: str | None = None, level: str | None = None
) -> dict[str, Any]:
"""Get PHP error log entries"""
if not self.wp_cli:
return {"success": False, "error": "WP-CLI not available"}
try:
# Get error log path
log_path_cmd = "eval 'echo ini_get(\"error_log\");'"
log_path_result = await self.wp_cli.execute_command(log_path_cmd)
log_path = log_path_result.get("output", "").strip()
if not log_path or log_path == "":
# Try WordPress debug log
wp_path_cmd = "eval 'echo WP_CONTENT_DIR;'"
wp_path_result = await self.wp_cli.execute_command(wp_path_cmd)
wp_content = wp_path_result.get("output", "").strip()
log_path = f"{wp_content}/debug.log"
# Get log file size
size_cmd = f"eval 'echo filesize(\"{log_path}\") ?? 0;'"
try:
size_result = await self.wp_cli.execute_command(size_cmd)
log_size_bytes = int(size_result.get("output", "0").strip())
log_size_mb = round(log_size_bytes / 1024 / 1024, 2)
except:
log_size_mb = 0.0
# Read log file (last N lines)
tail_cmd = f"eval 'exec(\"tail -n {lines} {log_path} 2>&1\");'"
log_result = await self.wp_cli.execute_command(tail_cmd)
log_lines = log_result.get("output", "").strip().split("\n")
# Parse log entries
entries = []
for line in log_lines:
if not line.strip():
continue
# Basic parsing (PHP error log format varies)
# Format: [timestamp] PHP Error_Type: message in file on line X
match = re.match(r"\[([^\]]+)\]\s+PHP\s+(\w+):\s+(.+)", line)
if match:
timestamp, error_type, message = match.groups()
# Extract file and line if present
file_match = re.search(r"in\s+(.+)\s+on\s+line\s+(\d+)", message)
file_path = file_match.group(1) if file_match else None
line_num = int(file_match.group(2)) if file_match else None
entry = {
"timestamp": timestamp,
"level": error_type.lower(),
"message": message,
"file": file_path,
"line": line_num,
}
# Apply filters
if level and entry["level"] != level.lower():
continue
if filter and filter.lower() not in message.lower():
continue
entries.append(entry)
else:
# Unparsed line - include as-is
entries.append(
{
"timestamp": "N/A",
"level": "unknown",
"message": line,
"file": None,
"line": None,
}
)
return {
"success": True,
"entries": entries,
"total_lines": len(log_lines),
"filtered_lines": len(entries),
"log_size_mb": log_size_mb,
"log_path": log_path,
}
except Exception as e:
self.logger.error(f"Error log retrieval failed: {str(e)}")
return {"success": False, "error": str(e)}

View File

@@ -0,0 +1,263 @@
"""
WordPress Advanced Plugin - Clean Architecture
Advanced WordPress management features requiring elevated permissions.
Provides database operations, bulk operations, and system management.
This plugin is separated from core WordPress plugin for:
- Better security (separate API keys for advanced features)
- Better tool visibility (users see only features they need)
- Granular access control
"""
import json
from typing import Any
from plugins.base import BasePlugin
from plugins.wordpress.client import WordPressClient
from plugins.wordpress_advanced import handlers
class WordPressAdvancedPlugin(BasePlugin):
"""
WordPress Advanced plugin - separated for security and visibility.
Provides advanced WordPress management capabilities:
- Database operations (export, import, search, query, repair)
- Bulk operations (batch updates/deletes for posts, products, media)
- System operations (system info, cache, cron, error logs)
Requires:
- WordPress site with REST API
- WP-CLI access (for database and system operations)
- Docker container name (for WP-CLI execution)
"""
@staticmethod
def get_plugin_name() -> str:
"""Return plugin type identifier"""
return "wordpress_advanced"
@staticmethod
def get_required_config_keys() -> list[str]:
"""Return required configuration keys"""
return ["url", "username", "app_password", "container"]
def __init__(self, config: dict[str, Any], project_id: str | None = None):
"""
Initialize WordPress Advanced plugin with handlers.
Args:
config: Configuration dictionary containing:
- url: WordPress site URL
- username: WordPress username
- app_password: WordPress application password
- container: Docker container name for WP-CLI (REQUIRED)
project_id: Optional project ID (auto-generated if not provided)
"""
super().__init__(config, project_id=project_id)
# Create WordPress API client
self.client = WordPressClient(
site_url=config["url"], username=config["username"], app_password=config["app_password"]
)
# WP-CLI is REQUIRED for wordpress_advanced
container_name = config.get("container")
if not container_name:
raise ValueError(
"WordPress Advanced plugin requires 'container' configuration. "
"Please set WORDPRESS_ADVANCED_SITE1_CONTAINER in environment variables."
)
# Import WP-CLI manager
from plugins.wordpress.wp_cli import WPCLIManager
wp_cli_manager = WPCLIManager(container_name)
# Initialize handlers (all require WP-CLI or advanced REST API)
self.database = handlers.DatabaseHandler(self.client, wp_cli_manager)
self.bulk = handlers.BulkHandler(self.client)
self.system = handlers.SystemHandler(self.client, wp_cli_manager)
@staticmethod
def get_tool_specifications() -> list[dict[str, Any]]:
"""
Return all tool specifications for ToolGenerator.
This method is called by ToolGenerator to create unified tools
with site parameter routing.
Returns:
List of tool specification dictionaries (22 tools total)
"""
specs = []
# Database operations (7 tools)
specs.extend(handlers.get_database_specs())
# Bulk operations (8 tools)
specs.extend(handlers.get_bulk_specs())
# System operations (7 tools)
specs.extend(handlers.get_system_specs())
return specs
async def health_check(self) -> dict[str, Any]:
"""
Check WordPress Advanced features availability.
Returns:
Dict with health status and WP-CLI availability
"""
try:
# Test WP-CLI access (primary requirement for wordpress_advanced)
wp_cli_version = await self.system.wp_cli_version()
wp_cli_available = bool(wp_cli_version.get("version"))
# Test REST API access with a public endpoint
rest_api_available = False
try:
# Use a public endpoint that doesn't require authentication
site_info = await self.client.get("/")
rest_api_available = bool(site_info.get("name"))
except Exception as e:
self.logger.warning(f"REST API check failed (non-critical): {e}")
rest_api_available = False
return {
"healthy": wp_cli_available, # Only WP-CLI is critical for wordpress_advanced
"wp_cli_available": wp_cli_available,
"rest_api_available": rest_api_available,
"features": {
"database_operations": wp_cli_available,
"bulk_operations": rest_api_available,
"system_operations": wp_cli_available,
},
}
except Exception as e:
return {
"healthy": False,
"error": str(e),
"wp_cli_available": False,
"rest_api_available": False,
}
# ========================================
# Method Delegation to Handlers
# ========================================
# All methods delegate to appropriate handlers
# This maintains backward compatibility with existing code
# === Database Operations (7 tools) ===
async def wp_db_export(self, **kwargs):
"""Export WordPress database"""
result = await self.database.wp_db_export(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_import(self, **kwargs):
"""Import WordPress database"""
result = await self.database.wp_db_import(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_size(self, **kwargs):
"""Get database size"""
result = await self.database.wp_db_size(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_tables(self, **kwargs):
"""List database tables"""
result = await self.database.wp_db_tables(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_search(self, **kwargs):
"""Search database"""
result = await self.database.wp_db_search(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_query(self, **kwargs):
"""Execute read-only database query"""
result = await self.database.wp_db_query(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def wp_db_repair(self, **kwargs):
"""Repair and optimize database"""
result = await self.database.wp_db_repair(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
# === Bulk Operations (8 tools) ===
async def bulk_update_posts(self, **kwargs):
"""Bulk update posts"""
result = await self.bulk.bulk_update_posts(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_delete_posts(self, **kwargs):
"""Bulk delete posts"""
result = await self.bulk.bulk_delete_posts(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_update_products(self, **kwargs):
"""Bulk update products"""
result = await self.bulk.bulk_update_products(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_delete_products(self, **kwargs):
"""Bulk delete products"""
result = await self.bulk.bulk_delete_products(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_delete_media(self, **kwargs):
"""Bulk delete media"""
result = await self.bulk.bulk_delete_media(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_assign_categories(self, **kwargs):
"""Bulk assign categories to posts"""
result = await self.bulk.bulk_assign_categories(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_assign_tags(self, **kwargs):
"""Bulk assign tags to posts"""
result = await self.bulk.bulk_assign_tags(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def bulk_trash_posts(self, **kwargs):
"""Bulk move posts to trash"""
result = await self.bulk.bulk_trash_posts(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
# === System Operations (7 tools) ===
async def system_info(self, **kwargs):
"""Get system information"""
result = await self.system.system_info(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def system_phpinfo(self, **kwargs):
"""Get PHP information"""
result = await self.system.system_phpinfo(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def system_disk_usage(self, **kwargs):
"""Get disk usage"""
result = await self.system.system_disk_usage(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def system_clear_all_caches(self, **kwargs):
"""Clear all caches"""
result = await self.system.system_clear_all_caches(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def cron_list(self, **kwargs):
"""List cron jobs"""
result = await self.system.cron_list(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def cron_run(self, **kwargs):
"""Run cron job"""
result = await self.system.cron_run(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result
async def error_log(self, **kwargs):
"""Get error log"""
result = await self.system.error_log(**kwargs)
return json.dumps(result, indent=2) if isinstance(result, dict) else result

View File

@@ -0,0 +1,34 @@
"""
WordPress Advanced Pydantic Schemas
"""
from .bulk import *
from .database import *
from .system import *
__all__ = [
# Database schemas
"DatabaseExportRequest",
"DatabaseImportRequest",
"DatabaseSizeResponse",
"DatabaseTablesResponse",
"DatabaseSearchRequest",
"DatabaseQueryRequest",
"DatabaseRepairResponse",
# Bulk schemas
"BulkUpdatePostsRequest",
"BulkDeletePostsRequest",
"BulkUpdateProductsRequest",
"BulkDeleteProductsRequest",
"BulkDeleteMediaRequest",
"BulkAssignCategoriesRequest",
"BulkAssignTagsRequest",
"BulkOperationResponse",
# System schemas
"SystemInfoResponse",
"SystemPHPInfoResponse",
"SystemDiskUsageResponse",
"CronListResponse",
"CronRunRequest",
"ErrorLogResponse",
]

View File

@@ -0,0 +1,250 @@
"""
Bulk Operations Schemas
Pydantic models for WordPress bulk operations including:
- Bulk updates for posts/products
- Bulk deletions
- Bulk category/tag assignments
"""
from typing import Any
from pydantic import BaseModel, Field, field_validator
class BulkUpdatePostsParams(BaseModel):
"""Parameters for bulk updating posts"""
post_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of post IDs to update (max 100)"
)
updates: dict[str, Any] = Field(
..., description="Fields to update (status, author_id, categories, tags, etc.)"
)
@classmethod
@field_validator("post_ids")
def validate_post_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All post IDs must be positive integers")
return v
@classmethod
@field_validator("updates")
def validate_updates(cls, v):
"""Validate update fields are allowed"""
allowed_fields = {
"status",
"title",
"content",
"excerpt",
"author",
"categories",
"tags",
"featured_media",
"comment_status",
"ping_status",
"sticky",
"format",
"meta",
}
invalid_fields = set(v.keys()) - allowed_fields
if invalid_fields:
raise ValueError(f"Invalid update fields: {', '.join(invalid_fields)}")
return v
class BulkDeletePostsParams(BaseModel):
"""Parameters for bulk deleting posts"""
post_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of post IDs to delete (max 100)"
)
force: bool = Field(default=False, description="Force permanent deletion (bypass trash)")
@classmethod
@field_validator("post_ids")
def validate_post_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All post IDs must be positive integers")
return v
class BulkUpdateProductsParams(BaseModel):
"""Parameters for bulk updating WooCommerce products"""
product_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of product IDs to update (max 100)"
)
updates: dict[str, Any] = Field(
..., description="Fields to update (price, stock_quantity, status, etc.)"
)
@classmethod
@field_validator("product_ids")
def validate_product_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All product IDs must be positive integers")
return v
@classmethod
@field_validator("updates")
def validate_updates(cls, v):
"""Validate update fields are allowed"""
allowed_fields = {
"name",
"status",
"featured",
"catalog_visibility",
"description",
"short_description",
"sku",
"price",
"regular_price",
"sale_price",
"stock_quantity",
"stock_status",
"manage_stock",
"categories",
"tags",
"images",
"attributes",
"meta_data",
}
invalid_fields = set(v.keys()) - allowed_fields
if invalid_fields:
raise ValueError(f"Invalid update fields: {', '.join(invalid_fields)}")
return v
class BulkDeleteProductsParams(BaseModel):
"""Parameters for bulk deleting products"""
product_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of product IDs to delete (max 100)"
)
force: bool = Field(default=False, description="Force permanent deletion")
@classmethod
@field_validator("product_ids")
def validate_product_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All product IDs must be positive integers")
return v
class BulkAssignCategoriesParams(BaseModel):
"""Parameters for bulk assigning categories"""
item_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of post/product IDs (max 100)"
)
category_ids: list[int] = Field(..., min_length=1, description="List of category IDs to assign")
replace: bool = Field(
default=False, description="Replace existing categories (true) or append (false)"
)
item_type: str = Field(default="post", description="Type of items: 'post' or 'product'")
@classmethod
@field_validator("item_ids", "category_ids")
def validate_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All IDs must be positive integers")
return v
@classmethod
@field_validator("item_type")
def validate_item_type(cls, v):
"""Validate item type"""
if v not in ["post", "product"]:
raise ValueError("item_type must be 'post' or 'product'")
return v
class BulkAssignTagsParams(BaseModel):
"""Parameters for bulk assigning tags"""
item_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of post/product IDs (max 100)"
)
tag_ids: list[int] = Field(..., min_length=1, description="List of tag IDs to assign")
replace: bool = Field(
default=False, description="Replace existing tags (true) or append (false)"
)
item_type: str = Field(default="post", description="Type of items: 'post' or 'product'")
@classmethod
@field_validator("item_ids", "tag_ids")
def validate_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All IDs must be positive integers")
return v
@classmethod
@field_validator("item_type")
def validate_item_type(cls, v):
"""Validate item type"""
if v not in ["post", "product"]:
raise ValueError("item_type must be 'post' or 'product'")
return v
class BulkUpdateMediaParams(BaseModel):
"""Parameters for bulk updating media items"""
media_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of media IDs to update (max 100)"
)
updates: dict[str, Any] = Field(
..., description="Fields to update (alt_text, title, caption, description)"
)
@classmethod
@field_validator("media_ids")
def validate_media_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All media IDs must be positive integers")
return v
@classmethod
@field_validator("updates")
def validate_updates(cls, v):
"""Validate update fields are allowed"""
allowed_fields = {"title", "alt_text", "caption", "description", "meta"}
invalid_fields = set(v.keys()) - allowed_fields
if invalid_fields:
raise ValueError(f"Invalid update fields: {', '.join(invalid_fields)}")
return v
class BulkDeleteMediaParams(BaseModel):
"""Parameters for bulk deleting media items"""
media_ids: list[int] = Field(
..., min_length=1, max_length=100, description="List of media IDs to delete (max 100)"
)
force: bool = Field(
default=True, description="Force permanent deletion (media can't be trashed)"
)
@classmethod
@field_validator("media_ids")
def validate_media_ids(cls, v):
"""Ensure all IDs are positive"""
if any(id <= 0 for id in v):
raise ValueError("All media IDs must be positive integers")
return v
class BulkOperationResult(BaseModel):
"""Result of a bulk operation"""
success_count: int = Field(description="Number of successful operations")
failed_count: int = Field(description="Number of failed operations")
total: int = Field(description="Total items processed")
failed_ids: list[int] = Field(default=[], description="IDs that failed to process")
errors: list[dict[str, Any]] = Field(default=[], description="Detailed error information")

View File

@@ -0,0 +1,173 @@
"""
Database Operations Schemas
Pydantic models for WordPress database operations including:
- Database export/import
- Backup/restore
- Optimization and repair
- Search and query operations
"""
from typing import Any
from pydantic import BaseModel, Field, field_validator
class DatabaseExportParams(BaseModel):
"""Parameters for database export"""
tables: list[str] | None = Field(
default=None, description="Specific tables to export (default: all tables)"
)
exclude_tables: list[str] | None = Field(
default=None, description="Tables to exclude from export"
)
add_drop_table: bool = Field(default=True, description="Include DROP TABLE statements")
@classmethod
@field_validator("tables", "exclude_tables")
def validate_table_names(cls, v):
"""Validate table names - no special characters"""
if v:
for table in v:
if not table.replace("_", "").isalnum():
raise ValueError(f"Invalid table name: {table}")
return v
class DatabaseImportParams(BaseModel):
"""Parameters for database import"""
file_path: str | None = Field(default=None, description="Path to SQL file on server")
url: str | None = Field(default=None, description="URL to download SQL file from")
skip_optimization: bool = Field(
default=False, description="Skip database optimization after import"
)
@classmethod
@field_validator("url")
def validate_url(cls, v):
"""Validate URL format"""
if v and not v.startswith(("http://", "https://")):
raise ValueError("URL must start with http:// or https://")
return v
class DatabaseBackupParams(BaseModel):
"""Parameters for creating database backup"""
description: str | None = Field(
default=None, max_length=255, description="Optional description for this backup"
)
compress: bool = Field(default=True, description="Compress backup with gzip")
include_uploads: bool = Field(
default=False, description="Also backup wp-content/uploads directory"
)
class DatabaseRestoreParams(BaseModel):
"""Parameters for restoring database"""
backup_id: str | None = Field(default=None, description="Backup ID to restore")
timestamp: str | None = Field(
default=None, description="Backup timestamp (alternative to backup_id)"
)
confirm: bool = Field(default=False, description="Confirmation required (safety check)")
@classmethod
@field_validator("confirm")
def validate_confirmation(cls, v):
"""Require explicit confirmation for restore"""
if not v:
raise ValueError("Confirmation required: set confirm=true")
return v
class DatabaseSearchParams(BaseModel):
"""Parameters for searching database"""
search_string: str = Field(
..., min_length=1, max_length=255, description="String to search for in database"
)
tables: list[str] | None = Field(
default=None, description="Specific tables to search (default: all tables)"
)
regex: bool = Field(default=False, description="Use regex pattern matching")
case_sensitive: bool = Field(default=False, description="Case-sensitive search")
max_results: int = Field(default=100, ge=1, le=1000, description="Maximum results to return")
class DatabaseQueryParams(BaseModel):
"""Parameters for executing SQL query"""
query: str = Field(
..., min_length=1, max_length=10000, description="SQL query to execute (SELECT only)"
)
max_rows: int = Field(default=1000, ge=1, le=10000, description="Maximum rows to return")
@classmethod
@field_validator("query")
def validate_query(cls, v):
"""
Validate query is safe (read-only SELECT)
Security: Only allow SELECT, SHOW, DESCRIBE statements
Prevent: INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, etc.
"""
query_upper = v.strip().upper()
# Allowed statement types
allowed_starts = ("SELECT", "SHOW", "DESCRIBE", "DESC", "EXPLAIN")
if not any(query_upper.startswith(cmd) for cmd in allowed_starts):
raise ValueError("Only SELECT, SHOW, DESCRIBE, and EXPLAIN queries allowed")
# Forbidden keywords (prevent nested destructive queries)
forbidden = [
"INSERT",
"UPDATE",
"DELETE",
"DROP",
"ALTER",
"CREATE",
"TRUNCATE",
"REPLACE",
"GRANT",
"REVOKE",
]
for keyword in forbidden:
if keyword in query_upper:
raise ValueError(f"Forbidden keyword in query: {keyword}")
return v
class DatabaseSizeResponse(BaseModel):
"""Response model for database size information"""
total_size_mb: float = Field(description="Total database size in MB")
tables: list[dict[str, Any]] = Field(description="List of tables with size information")
row_count: int = Field(description="Total row count across all tables")
class DatabaseTableInfo(BaseModel):
"""Information about a database table"""
name: str
engine: str
rows: int
data_size_mb: float
index_size_mb: float
total_size_mb: float
collation: str
class DatabaseRepairResult(BaseModel):
"""Result of database repair operation"""
table: str
status: str # 'OK', 'Repaired', 'Failed'
message: str | None = None
class DatabaseBackupInfo(BaseModel):
"""Information about a database backup"""
backup_id: str
timestamp: str
size_mb: float
compressed: bool
description: str | None = None
location: str
tables_count: int

View File

@@ -0,0 +1,140 @@
"""
System Operations Schemas
Pydantic models for WordPress system operations including:
- System information
- Disk usage
- Cron management
- Cache operations
- Error logs
"""
from typing import Any
from pydantic import BaseModel, Field, field_validator
class SystemInfoResponse(BaseModel):
"""Comprehensive system information"""
php_version: str = Field(description="PHP version")
mysql_version: str = Field(description="MySQL/MariaDB version")
wordpress_version: str = Field(description="WordPress core version")
server_software: str = Field(description="Web server software")
memory_limit: str = Field(description="PHP memory limit")
max_execution_time: int = Field(description="Max execution time in seconds")
upload_max_filesize: str = Field(description="Maximum upload file size")
post_max_size: str = Field(description="Maximum POST size")
max_input_vars: int = Field(description="Maximum input variables")
php_extensions: list[str] = Field(default=[], description="Loaded PHP extensions")
wp_debug: bool = Field(description="WP_DEBUG constant status")
wp_debug_log: bool = Field(description="WP_DEBUG_LOG constant status")
multisite: bool = Field(description="WordPress Multisite enabled")
active_plugins: int = Field(description="Number of active plugins")
active_theme: str = Field(description="Active theme name")
class PHPInfoResponse(BaseModel):
"""PHP configuration details"""
version: str
sapi: str = Field(description="Server API (e.g., fpm-fcgi, apache2handler)")
extensions: list[str] = Field(description="Loaded PHP extensions")
ini_settings: dict[str, str] = Field(description="Important php.ini settings")
disabled_functions: list[str] = Field(default=[], description="Disabled PHP functions")
class DiskUsageResponse(BaseModel):
"""Disk usage statistics"""
total_size_mb: float = Field(description="Total disk usage in MB")
wordpress_size_mb: float = Field(description="WordPress installation size")
uploads_size_mb: float = Field(description="wp-content/uploads size")
plugins_size_mb: float = Field(description="wp-content/plugins size")
themes_size_mb: float = Field(description="wp-content/themes size")
database_size_mb: float = Field(description="Database size")
available_space_mb: float | None = Field(
default=None, description="Available disk space (if accessible)"
)
breakdown: dict[str, float] = Field(default={}, description="Detailed breakdown by directory")
class CronEvent(BaseModel):
"""WordPress cron event information"""
hook: str = Field(description="Cron hook name")
timestamp: int = Field(description="Unix timestamp of next run")
schedule: str = Field(description="Schedule type (hourly, daily, etc.)")
interval: int | None = Field(
default=None, description="Interval in seconds for recurring events"
)
args: list[Any] = Field(default=[], description="Arguments passed to the hook")
class CronListResponse(BaseModel):
"""List of all cron events"""
events: list[CronEvent] = Field(description="List of cron events")
total: int = Field(description="Total number of events")
schedules: dict[str, dict[str, Any]] = Field(default={}, description="Available cron schedules")
class CronRunParams(BaseModel):
"""Parameters for manually running a cron job"""
hook: str = Field(..., min_length=1, max_length=255, description="Cron hook name to execute")
args: list[Any] = Field(default=[], description="Optional arguments to pass to the hook")
@classmethod
@field_validator("hook")
def validate_hook(cls, v):
"""Validate hook name format"""
# Hook names should be alphanumeric with underscores, hyphens
if not v.replace("_", "").replace("-", "").replace(".", "").isalnum():
raise ValueError(
"Hook name can only contain letters, numbers, underscores, " "hyphens, and dots"
)
return v
class ErrorLogParams(BaseModel):
"""Parameters for retrieving error log"""
lines: int = Field(
default=100, ge=1, le=1000, description="Number of log lines to retrieve (max 1000)"
)
filter: str | None = Field(
default=None, description="Filter logs by keyword (case-insensitive)"
)
level: str | None = Field(
default=None, description="Filter by error level (error, warning, notice)"
)
@classmethod
@field_validator("level")
def validate_level(cls, v):
"""Validate error level"""
if v and v.lower() not in ["error", "warning", "notice", "fatal"]:
raise ValueError("level must be: error, warning, notice, or fatal")
return v.lower() if v else v
class ErrorLogEntry(BaseModel):
"""Single error log entry"""
timestamp: str = Field(description="Error timestamp")
level: str = Field(description="Error level (error, warning, etc.)")
message: str = Field(description="Error message")
file: str | None = Field(default=None, description="File where error occurred")
line: int | None = Field(default=None, description="Line number")
class ErrorLogResponse(BaseModel):
"""Error log retrieval response"""
entries: list[ErrorLogEntry] = Field(description="Log entries")
total_lines: int = Field(description="Total lines in log file")
filtered_lines: int = Field(description="Number of entries returned")
log_size_mb: float = Field(description="Log file size in MB")
class CacheStats(BaseModel):
"""Cache statistics"""
cache_type: str = Field(description="Type of object cache (Redis, Memcached, etc.)")
cache_enabled: bool = Field(description="Is persistent object cache enabled")
transients_count: int = Field(description="Number of transients in database")
opcache_enabled: bool = Field(description="Is OPcache enabled")
opcache_memory_usage: dict[str, Any] | None = Field(
default=None, description="OPcache memory usage statistics"
)