fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed
- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner - P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var - P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup - P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns - P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs) - P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import - P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default - P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed - P2: 28 frontend TODOs (hardcoded constants, deprecated notification API) - P3: dead code, duplicates, deprecated imports, private attr, __import__ inline - P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n) - ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix) - F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String) - Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
This commit is contained in:
@@ -4,18 +4,98 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import UTC
|
||||
from typing import Any
|
||||
|
||||
from app.plugins.base import BasePlugin
|
||||
from app.plugins.manifest import PluginManifest, PluginRouteDef, FrontendMenuItem, FrontendPageRoute, FrontendDetailTab, FrontendSettingsPage
|
||||
from app.plugins.manifest import (
|
||||
FrontendDetailTab,
|
||||
FrontendMenuItem,
|
||||
FrontendPageRoute,
|
||||
FrontendSettingsPage,
|
||||
PluginManifest,
|
||||
PluginRouteDef,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _mail_restore_handler(
|
||||
db, entity, action: str, snapshot: dict, context: dict,
|
||||
) -> dict:
|
||||
"""Special restore handler for Mail entities (moved from core, P0-7 fix).
|
||||
|
||||
Mail restore has IMAP semantics:
|
||||
- delete: move back from trash to original folder (if folder still exists)
|
||||
- update: revert metadata fields
|
||||
- create: soft-delete (undo send only works for drafts)
|
||||
"""
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
user_id = context.get("user_id")
|
||||
tenant_id = context.get("tenant_id")
|
||||
|
||||
if action == "delete":
|
||||
if entity is None:
|
||||
raise ValueError("Mail entity not found for restore")
|
||||
entity.deleted_at = None
|
||||
if user_id:
|
||||
entity.updated_by = user_id if hasattr(entity, "updated_by") else None
|
||||
original_folder_id = snapshot.get("folder_id")
|
||||
if original_folder_id and hasattr(entity, "folder_id"):
|
||||
try:
|
||||
folder_uuid = uuid.UUID(str(original_folder_id))
|
||||
from app.plugins.builtins.mail.models import MailFolder
|
||||
folder_q = select(MailFolder).where(
|
||||
MailFolder.id == folder_uuid,
|
||||
MailFolder.tenant_id == tenant_id,
|
||||
MailFolder.deleted_at.is_(None),
|
||||
)
|
||||
folder_result = await db.execute(folder_q)
|
||||
folder = folder_result.scalar_one_or_none()
|
||||
if folder:
|
||||
entity.folder_id = folder_uuid
|
||||
else:
|
||||
logger.warning(
|
||||
"Original mail folder %s no longer exists, "
|
||||
"restoring mail without folder assignment",
|
||||
original_folder_id,
|
||||
)
|
||||
except (ValueError, Exception) as e:
|
||||
logger.warning("Failed to restore mail folder: %s", e)
|
||||
await db.flush()
|
||||
return {"id": str(entity.id), "restored": True, "entity_type": "mail"}
|
||||
|
||||
elif action == "update":
|
||||
if entity is None:
|
||||
raise ValueError("Mail entity not found for restore")
|
||||
from app.core.restore_registry import _DEFAULT_EXCLUDED
|
||||
excluded = _DEFAULT_EXCLUDED | {
|
||||
"message_id", "rfc822_size", "raw_path", "account_id", "folder_id",
|
||||
}
|
||||
for key, value in snapshot.items():
|
||||
if hasattr(entity, key) and key not in excluded:
|
||||
setattr(entity, key, value)
|
||||
await db.flush()
|
||||
return {"id": str(entity.id), "restored": True, "entity_type": "mail"}
|
||||
|
||||
elif action == "create":
|
||||
if entity is None:
|
||||
raise ValueError("Mail entity not found for restore")
|
||||
entity.deleted_at = datetime.now(UTC)
|
||||
await db.flush()
|
||||
return {"id": str(entity.id), "restored": True, "entity_type": "mail", "note": "soft-deleted (undo create)"}
|
||||
|
||||
raise ValueError(f"Unsupported action for mail restore: {action}")
|
||||
|
||||
|
||||
async def _auto_sync_loop() -> None:
|
||||
"""Background loop: process pending sync queue, then sync all active mail accounts every 5 minutes."""
|
||||
from app.plugins.builtins.mail.services import auto_sync_all_accounts, process_sync_queue
|
||||
from app.core.db import get_session_factory
|
||||
from app.plugins.builtins.mail.services import auto_sync_all_accounts, process_sync_queue
|
||||
|
||||
while True:
|
||||
try:
|
||||
@@ -78,9 +158,29 @@ class MailPlugin(BasePlugin):
|
||||
async def on_activate(
|
||||
self, db, service_container, event_bus
|
||||
) -> None:
|
||||
"""Activate plugin: register events + start auto-sync background task."""
|
||||
"""Activate plugin: register events, restore, history + start auto-sync."""
|
||||
await super().on_activate(db, service_container, event_bus)
|
||||
|
||||
# Register restore config for Mail entities (P0-7 fix)
|
||||
from app.core.restore_registry import RestoreConfig, get_restore_registry
|
||||
from app.plugins.builtins.mail.models import Mail
|
||||
get_restore_registry().register(RestoreConfig(
|
||||
entity_type="mail",
|
||||
model_class=Mail,
|
||||
restore_permission="mail:write",
|
||||
excluded_fields=frozenset({"message_id", "rfc822_size", "raw_path", "account_id", "folder_id"}),
|
||||
special_handler=_mail_restore_handler,
|
||||
))
|
||||
|
||||
# Register history hooks for Mail entities (P0-8 fix)
|
||||
from app.core.history_hooks import register_history_hooks
|
||||
from app.core.hooks import get_hook_registry
|
||||
register_history_hooks(
|
||||
get_hook_registry(), "mail",
|
||||
"mail.after_create", "mail.after_update", "mail.after_delete",
|
||||
owner_tag="mail",
|
||||
)
|
||||
|
||||
if self._auto_sync_task is None or self._auto_sync_task.done():
|
||||
self._auto_sync_task = asyncio.create_task(_auto_sync_loop())
|
||||
logger.info("Mail plugin: auto-sync background task started")
|
||||
@@ -117,4 +217,14 @@ class MailPlugin(BasePlugin):
|
||||
self._auto_sync_task = None
|
||||
logger.info("Mail plugin: auto-sync background task stopped")
|
||||
|
||||
# Unregister history hooks (free functions, not bound methods)
|
||||
from app.core.hooks import get_hook_registry
|
||||
get_hook_registry().unregister_actions_by_owner("mail.after_create", "mail")
|
||||
get_hook_registry().unregister_actions_by_owner("mail.after_update", "mail")
|
||||
get_hook_registry().unregister_actions_by_owner("mail.after_delete", "mail")
|
||||
|
||||
# Unregister restore config for Mail entities
|
||||
from app.core.restore_registry import get_restore_registry
|
||||
get_restore_registry().unregister("mail")
|
||||
|
||||
await super().on_deactivate(db, service_container, event_bus)
|
||||
|
||||
Reference in New Issue
Block a user