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:
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)}
|
||||
Reference in New Issue
Block a user