Initial commit: MCP Hub Community Edition v3.0.0

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

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

View File

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

View File

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

View File

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

View File

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