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:
375
plugins/wordpress_advanced/README.md
Normal file
375
plugins/wordpress_advanced/README.md
Normal 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
|
||||
12
plugins/wordpress_advanced/__init__.py
Normal file
12
plugins/wordpress_advanced/__init__.py
Normal 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"]
|
||||
26
plugins/wordpress_advanced/handlers/__init__.py
Normal file
26
plugins/wordpress_advanced/handlers/__init__.py
Normal 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",
|
||||
]
|
||||
687
plugins/wordpress_advanced/handlers/bulk.py
Normal file
687
plugins/wordpress_advanced/handlers/bulk.py
Normal 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)}
|
||||
589
plugins/wordpress_advanced/handlers/database.py
Normal file
589
plugins/wordpress_advanced/handlers/database.py
Normal 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)}
|
||||
580
plugins/wordpress_advanced/handlers/system.py
Normal file
580
plugins/wordpress_advanced/handlers/system.py
Normal 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)}
|
||||
263
plugins/wordpress_advanced/plugin.py
Normal file
263
plugins/wordpress_advanced/plugin.py
Normal 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
|
||||
34
plugins/wordpress_advanced/schemas/__init__.py
Normal file
34
plugins/wordpress_advanced/schemas/__init__.py
Normal 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",
|
||||
]
|
||||
250
plugins/wordpress_advanced/schemas/bulk.py
Normal file
250
plugins/wordpress_advanced/schemas/bulk.py
Normal 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")
|
||||
173
plugins/wordpress_advanced/schemas/database.py
Normal file
173
plugins/wordpress_advanced/schemas/database.py
Normal 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
|
||||
140
plugins/wordpress_advanced/schemas/system.py
Normal file
140
plugins/wordpress_advanced/schemas/system.py
Normal 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"
|
||||
)
|
||||
Reference in New Issue
Block a user