# Moderation Expert Agent

> **Specialized agent for Forum Press Zone moderation system development**
> Expertise: Bans, warnings, mutes, reports, content filtering, audit logging

---

## Identity & Scope

**Name:** `moderation-expert`
**Domain:** Moderation system (bans, warnings, reports, content validation)
**Primary Files:**
- `includes/class-presszone-forum-bans.php` - Ban system
- `includes/class-presszone-forum-warnings.php` - Warning/mute system
- `includes/class-presszone-forum-reports.php` - Report queue handling
- `includes/class-presszone-forum-post-creator.php` - Content validation methods
- `admin/src-vanilla/pages/moderation.js` - Moderation hub (redirects to bans.js)
- `admin/src-vanilla/pages/bans.js` - Full moderation UI with tabs

---

## Tech Stack

### Backend
| Technology | Details |
|------------|---------|
| **PHP** | 8.0+ with strict types |
| **Framework** | WordPress REST API |
| **Namespace** | `PresszoneForumPlugin` |
| **Tables** | `presszone_forum_warnings`, `presszone_forum_reports` |

### Frontend (Admin SPA)
| Technology | Details |
|------------|---------|
| **JavaScript** | Vanilla ES6+ (NO framework) |
| **CSS** | Plain CSS with `--presszone-forum-*` variables |
| **Routing** | Hash-based: `#/moderation/overview`, `#/moderation/reports`, etc. |
| **Entry** | `admin/src-vanilla/pages/bans.js` |

---

## WordPress.org Compliance Rules

### Security Requirements

1. **Audit Logging** - All mod actions MUST be logged
2. **Permission Checks** - Always verify `Roles::canModerate()` before actions
3. **Nonce Verification** - Use `check_ajax_referer()` for AJAX, REST nonces for API
4. **Input Sanitization** - Use `sanitize_textarea_field()`, `absint()`, etc.
5. **Prepared Statements** - Always use `$wpdb->prepare()` for queries

### JavaScript Naming (CRITICAL)

```javascript
// CORRECT - 4+ character descriptive names
const PresszoneForumApp = {};
window.presszoneForumData = {};

// FORBIDDEN - Will cause plugin rejection
const FPZ = {};      // 3 letters - TOO SHORT
const pz = {};       // 2 letters - TOO SHORT
```

---

## Ban System (`Bans` Class)

### Constants

```php
// User meta keys
private const META_BANNED = 'presszone_forum_banned';
private const META_BAN_EXPIRES = 'presszone_forum_ban_expires';
private const META_BAN_REASON = 'presszone_forum_ban_reason';
private const META_BANNED_BY = 'presszone_forum_banned_by';
private const META_BANNED_AT = 'presszone_forum_banned_at';
private const META_BAN_NOTES = 'presszone_forum_ban_notes';

// Predefined durations (seconds)
public const DURATION_1_DAY = 86400;
public const DURATION_1_WEEK = 604800;
public const DURATION_1_MONTH = 2592000;
public const DURATION_PERMANENT = 'permanent';
```

### Key Methods

| Method | Signature | Purpose |
|--------|-----------|---------|
| `ban()` | `static ban(int $userId, $duration, string $reason = '', string $notes = '', ?int $bannedBy = null): bool` | Ban a user |
| `unban()` | `static unban(int $userId): bool` | Remove ban |
| `isBanned()` | `static isBanned(int $userId): bool` | Check if currently banned |
| `getBanInfo()` | `static getBanInfo(int $userId): ?array` | Get full ban details |
| `getBannedUsers()` | `static getBannedUsers(int $limit = 50, int $offset = 0): array` | List banned users |
| `getBanMessage()` | `static getBanMessage(int $userId): string` | User-facing ban message |

### Ban Patterns

```php
// Ban a user for 1 week
Bans::ban($userId, Bans::DURATION_1_WEEK, 'Spam', 'Internal notes', get_current_user_id());

// Permanent ban
Bans::ban($userId, 'permanent', 'Repeated violations');

// Check before allowing actions
if (Bans::isBanned($userId)) {
    return new WP_Error('banned', Bans::getBanMessage($userId));
}

// Auto-unban on expiry (handled in isBanned())
// When checking isBanned(), expired bans are automatically cleared
```

### Protection Rules

```php
// Administrators CANNOT be banned
if (user_can($userId, 'administrator')) {
    return false;
}
```

---

## Warning System (`Warnings` Class)

### Types

```php
public const TYPE_WARNING = 'warning';  // Note only, no restriction
public const TYPE_MUTE = 'mute';        // Cannot post
public const TYPE_BAN = 'ban';          // Full ban (uses Bans class)
```

### Mute Durations

```php
public const DURATION_1_HOUR = 3600;
public const DURATION_24_HOURS = 86400;
public const DURATION_1_WEEK = 604800;
```

### Key Methods

| Method | Signature | Purpose |
|--------|-----------|---------|
| `issue()` | `static issue(int $userId, string $type, string $reason, ?int $duration, ?int $postId, ?int $issuedBy, ?string $moderatorNotes): int\|false` | Issue warning/mute/ban |
| `getWarnings()` | `static getWarnings(int $userId, bool $activeOnly = false): array` | User's warnings |
| `getActiveMute()` | `static getActiveMute(int $userId): ?array` | Active mute info |
| `canPost()` | `static canPost(int $userId): bool` | Check if can post |
| `clear()` | `static clear(int $warningId): bool` | Deactivate a warning |
| `getSuggestedAction()` | `static getSuggestedAction(int $userId): array` | AI-style escalation suggestion |
| `getEscalationLevel()` | `static getEscalationLevel(int $userId): int` | 0-4 escalation level |

### Warning Patterns

```php
// Issue a warning (note only)
Warnings::issue($userId, Warnings::TYPE_WARNING, 'Off-topic post', null, $postId, get_current_user_id());

// Issue a 24-hour mute
Warnings::issue($userId, Warnings::TYPE_MUTE, 'Spam behavior', 86400, null, get_current_user_id(), 'Third offense this week');

// Check if user can post
if (!Warnings::canPost($userId)) {
    $message = Warnings::getMuteMessage($userId);
    wp_send_json_error(['message' => $message], 403);
}

// Get suggested action based on history
$suggested = Warnings::getSuggestedAction($userId);
// Returns: ['type' => 'mute', 'duration' => 86400, 'rationale' => 'User has 3 warnings...']
```

### Escalation Logic

| Level | Status | Description |
|-------|--------|-------------|
| 0 | Clean | No infractions |
| 1 | Warned | Has warnings |
| 2 | Muted | Has been muted |
| 3 | Banned | Has been banned |
| 4 | Perm Candidate | 3+ bans, permanent ban recommended |

### Configurable Thresholds (Options)

| Option | Default | Purpose |
|--------|---------|---------|
| `presszone_forum_warning_threshold_mute` | 3 | Warnings before suggesting mute |
| `presszone_forum_warning_threshold_ban` | 5 | Warnings before suggesting ban |
| `presszone_forum_permanent_ban_threshold` | 3 | Bans before suggesting permanent |
| `presszone_forum_default_mute_duration` | 86400 | Default mute length (seconds) |
| `presszone_forum_default_ban_duration` | 604800 | Default ban length (seconds) |

---

## Content Validation (CRITICAL - Use PostCreator)

### NEVER Duplicate Validation Logic

**ALWAYS use `PostCreator` methods. NEVER inline validation code.**

```php
// CORRECT - Reuse centralized methods
$creator = new PostCreator();
try {
    $creator->checkExternalLinks($content);  // Blocks external URLs
    $creator->checkWordFilter($content);      // Blocks banned words
} catch (\InvalidArgumentException $e) {
    // Handle error
}

// WRONG - Duplicating validation logic inline
$urlPattern = '#(?:https?://|www\.)[^\s<"\')\]]+#i';
if (preg_match($urlPattern, $message)) { ... }  // NO! Use checkExternalLinks()
```

### Available Validation Methods

| Method | Purpose | Throws |
|--------|---------|--------|
| `checkExternalLinks($content)` | Blocks external URLs when setting disabled | `InvalidArgumentException` |
| `checkWordFilter($content)` | Blocks banned words/phrases | `InvalidArgumentException` |
| `sanitizeMessage($content)` | HTML to BBCode conversion, sanitization | - |
| `parseMessage($content)` | BBCode to HTML for display | - |

### Word Filter Pattern

Word filter supports wildcards:

```php
// In admin settings: word filter option is comma-separated
// Examples:
// "spam" = matches only whole word "spam" (not "antispam")
// "*fuck*" = matches anywhere (fucking, motherfuck)
// "*fuck" = matches at word end
// "fuck*" = matches at word start
```

### External Links Check

```php
// Option: presszone_forum_allow_external_links ('1' or '0')
// When '0', blocks all non-site URLs:
// - http://example.com
// - https://example.com
// - www.example.com
```

---

## Report Queue (`Reports` Class)

### Report Statuses

| Status | Description |
|--------|-------------|
| `open` | Pending review |
| `resolved` | Action taken |
| `rejected` | Dismissed, no action needed |

### REST Endpoints

| Route | Method | Permission | Purpose |
|-------|--------|------------|---------|
| `/reports` | GET | Moderator | List reports |
| `/reports/{id}/resolve` | POST | Moderator | Mark resolved |
| `/reports/{id}/dismiss` | POST | Moderator | Dismiss report |
| `/reports/{id}/delete-content` | POST | Moderator | Soft-delete content, resolve |
| `/reports/{id}/delete-thread` | POST | Moderator | Delete entire thread, resolve |

### Report Workflow

```php
// 1. User submits report (AJAX)
// Action: wp_ajax_presszone_forum_report_content

// 2. Moderator views queue
$reports = new Reports();
$data = $reports->getReports($request);  // Filtered by moderator's access level

// 3. Moderator takes action
// - resolve: Mark as handled, no content change
// - dismiss: Mark as rejected, no action
// - delete-content: Soft-delete the post, resolve
// - delete-thread: Soft-delete entire thread + all posts, resolve

// 4. Soft delete pattern
$wpdb->update(
    $this->prefix . 'posts',
    [
        'is_soft_deleted' => 1,
        'deleted_at' => current_time('mysql', true),
        'deleted_by' => get_current_user_id()
    ],
    ['post_id' => $contentId]
);
```

### Access Control

Forum Moderators see reports from assigned forums only:

```php
if (!Roles::canModerateAll()) {
    $moderators = new Moderators();
    $nodeIds = $moderators->getAllModeratedNodes(get_current_user_id());
    // Filter reports by node_id in SQL
}
```

---

## Nested Content Parsing (BBCode)

### Use Negative Lookahead for Nested Structures

```php
// CORRECT - Matches innermost quotes first (no nested [quote inside)
'#\[quote="([^"]+)"\]((?:(?!\[quote).)*?)\[/quote\]#is'

// WRONG - Matches first [/quote] found, breaks nesting
'#\[quote="([^"]+)"\](.*?)\[/quote\]#is'
```

Same for HTML blockquotes:

```php
// CORRECT - Innermost blockquotes first
'/<blockquote[^>]*data-author=["\']([^"\']+)["\'][^>]*>((?:(?!<blockquote).)*?)<\/blockquote>/is'
```

---

## Admin UI (bans.js)

### Tab Structure

```javascript
const tabDefs = [
    { id: 'overview', label: 'Overview', icon: '...' },     // Stats + quick actions
    { id: 'reports', label: 'Reports', icon: '...' },       // Report queue
    { id: 'warnings', label: 'User Lookup', icon: '...' },  // Search user, view history
    { id: 'warned', label: 'Warned', icon: '...' },         // Users with warnings
    { id: 'muted', label: 'Muted', icon: '...' },           // Currently muted users
    { id: 'bans', label: 'Banned', icon: '...' }            // Currently banned users
];
```

### Infraction Configuration

```javascript
const INFRACTION_CONFIG = {
    ban: {
        endpoint: '/bans',
        removeAction: 'unban',
        reasonField: 'ban_reason',
        durationField: 'ban_duration_human',
        // ...
    },
    mute: {
        endpoint: '/mutes',
        removeAction: 'unmute',
        reasonField: 'mute_reason',
        // ...
    },
    warning: {
        endpoint: '/warnings/active',
        removeAction: 'clear-warning',
        reasonField: 'warning_reason',
        // ...
    }
};
```

### UI Components Used

| Component | Usage |
|-----------|-------|
| `Tabs` | Tab navigation |
| `GridTable` | User lists |
| `Modal` | Issue infraction dialog |
| `UserAutocomplete` | User search |
| `Button` | Actions (Ban, Warn, Resolve, etc.) |
| `Confirm` | Confirmation dialogs |

### URL Deep Linking

```javascript
// Navigate to user with pending action
// #/moderation/warnings?user=123&action=ban

function getHashParams() {
    const hash = window.location.hash;
    const queryIndex = hash.indexOf('?');
    // Parse params...
}
```

---

## REST API Patterns

### Permission Callbacks

```php
// Moderator permission (admin, super mod, or forum mod)
'permission_callback' => function () {
    return Roles::canModerate();
}

// Full moderator access (admin, super mod only)
'permission_callback' => function () {
    return Roles::canModerateAll();
}
```

### Issuing Infraction (POST /users/{id}/warnings)

```php
$data = [
    'type' => 'mute',           // warning | mute | ban
    'reason' => 'Spam behavior',
    'duration' => 86400,        // null for permanent/warning
    'moderator_notes' => 'Third offense'  // Internal only
];
```

### Audit Hooks

```php
// Hook into actions for logging
do_action('presszone_forum_user_banned', $userId, $expires, $reason, $notes, $bannedBy);
do_action('presszone_forum_user_unbanned', $userId);
do_action('presszone_forum_warning_issued', $warningId, $userId, $type, $reason);
```

---

## Database Tables

### Warnings Table (`presszone_forum_warnings`)

| Column | Type | Description |
|--------|------|-------------|
| `warning_id` | INT | Primary key |
| `user_id` | INT | Target user |
| `issued_by` | INT | Moderator ID |
| `warning_type` | VARCHAR | warning, mute, ban |
| `reason` | TEXT | User-visible reason |
| `moderator_notes` | TEXT | Internal notes |
| `post_id` | INT | Related post (optional) |
| `duration` | INT | Duration in seconds |
| `expires_at` | DATETIME | When expires (null = never) |
| `is_active` | TINYINT | 1 = active, 0 = cleared |
| `created_at` | DATETIME | When issued |

### Reports Table (`presszone_forum_reports`)

| Column | Type | Description |
|--------|------|-------------|
| `report_id` | INT | Primary key |
| `content_type` | VARCHAR | post, thread |
| `content_id` | INT | Post/thread ID |
| `reporter_id` | INT | Who reported |
| `reason` | TEXT | Report reason |
| `status` | VARCHAR | open, resolved, rejected |
| `resolve_date` | DATETIME | When resolved |
| `resolver_id` | INT | Who resolved |
| `resolution_note` | TEXT | Mod notes on resolution |

---

## Email Notifications

### Ban Notification

```php
// Template placeholders:
// {username}, {reason}, {expires}, {site_name}

// Options:
// presszone_forum_banned_email_subject
// presszone_forum_banned_email_body
```

### Warning/Mute Notification

```php
// Options:
// presszone_forum_enable_warning_emails (default: true)
// presszone_forum_warning_email_subject
// presszone_forum_warning_email_body
// presszone_forum_mute_email_subject
// presszone_forum_mute_email_body

// Template placeholders:
// {username}, {reason}, {site_name}, {duration}, {expires}
```

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Duplicating validation logic | Use `PostCreator->checkExternalLinks()` and `checkWordFilter()` |
| Using `.*?` for nested BBCode | Use `(?:(?!\[quote).)*?` negative lookahead |
| Not checking admin protection | Never ban users with `administrator` capability |
| Forgetting soft delete fields | Always set `is_soft_deleted`, `deleted_at`, `deleted_by` |
| Missing audit hooks | Always fire `do_action()` for logging |
| Skipping permission check | Always verify `Roles::canModerate()` first |
| Hardcoded durations in UI | Use duration constants or configurable options |
| Not refreshing user stats | Call `Maintenance->recountUserStats()` after delete |
| Using short JS prefixes | Use `presszoneForumData`, not `FPZ` |
| Missing Toast for errors | Always show `Toast.error()` for user-facing errors |
| Not syncing TinyMCE | Call `tinymce.triggerSave()` before form submit |
| Inline notification code | Use `Toast` component from `components/Toast.js` |

---

## Self-Learning Protocol

### When to Update This File

1. **New moderation feature** - Add to relevant section
2. **New validation method** - Add to Content Validation
3. **New REST endpoint** - Add to REST API Patterns
4. **Bug pattern identified** - Add to Common Mistakes
5. **New escalation rule** - Update Escalation Logic section
6. **Database schema change** - Update Database Tables

### Update Format

Add entries to Recent Updates below with date + description.

---

## Recent Updates

*Tracks changes to agent knowledge base.*

- **2025-01-04** - Initial creation with full moderation system knowledge

---

## Quick Reference

### Check Before Any Mod Action

```php
// 1. Permission check
if (!Roles::canModerate()) {
    return $this->respondError('forbidden', 'Access denied', 403);
}

// 2. Target protection (for bans/warnings)
if (user_can($userId, 'administrator')) {
    return $this->respondError('forbidden', 'Cannot moderate administrators', 403);
}

// 3. Sanitize inputs
$reason = sanitize_textarea_field(wp_unslash($request->get_param('reason')));
$duration = absint($request->get_param('duration'));
```

### Content Validation Flow

```php
$creator = new PostCreator();
try {
    // 1. Check external links (if restricted)
    $creator->checkExternalLinks($content);

    // 2. Check word filter
    $creator->checkWordFilter($content);

    // 3. Sanitize for storage
    $sanitized = $creator->sanitizeMessage($content);

    // 4. Parse for display
    $html = $creator->parseMessage($sanitized);
} catch (\InvalidArgumentException $e) {
    // AJAX: wp_send_json_error(['message' => $e->getMessage()], 400);
    // REST: return $this->respondError('validation', $e->getMessage(), 400);
}
```

### Moderation Action Pattern

```php
// Issue infraction
$warningId = Warnings::issue(
    $userId,
    Warnings::TYPE_MUTE,
    $reason,
    $duration,
    $postId,
    get_current_user_id(),
    $moderatorNotes
);

// Action fires automatically via do_action()
// Email sent automatically

// Return success
return $this->respondSuccess(['id' => $warningId], 201);
```

---

## 🔒 MANDATORY SECURITY & COMPLIANCE RULES

### WordPress.org Compliance (ZERO TOLERANCE)

#### JavaScript Naming - 4+ Characters REQUIRED
```javascript
// CORRECT - WordPress.org compliant (4+ chars)
const PresszoneForumApp = {};
window.presszoneForumData = {};

// FORBIDDEN - Will cause plugin rejection
const FPZ = {};      // 3 letters - TOO SHORT
const pz = {};       // 2 letters - TOO SHORT
```

#### Input Sanitization - MANDATORY
```php
// ALWAYS sanitize ALL input before processing
$userId = absint($_POST['user_id']);
$reason = sanitize_textarea_field(wp_unslash($_POST['reason']));
$duration = absint($_POST['duration']);
$notes = sanitize_textarea_field(wp_unslash($_POST['moderator_notes']));

// ALWAYS validate arrays
$postIds = array_map('absint', $_POST['post_ids'] ?? []);
$postIds = array_filter($postIds); // Remove zeros

// FORBIDDEN - Direct use of user input
$reason = $_POST['reason'];  // NEVER - must sanitize
```

#### Permission Validation - CRITICAL
```php
// ALWAYS check permissions before moderation actions
if (!Roles::canModerate()) {
    return $this->respondError('forbidden', 'Access denied', 403);
}

// ALWAYS protect administrators from moderation
if (user_can($userId, 'administrator')) {
    return $this->respondError('forbidden', 'Cannot moderate administrators', 403);
}

// ALWAYS verify moderator can access the content's forum
if (!Roles::canModerateAll()) {
    $moderators = new Moderators();
    $nodeIds = $moderators->getAllModeratedNodes(get_current_user_id());
    
    if (!in_array($contentNodeId, $nodeIds, true)) {
        return $this->respondError('forbidden', 'Cannot moderate this forum', 403);
    }
}
```

### Security Rules - ABSOLUTE REQUIREMENTS

#### Audit Logging - MANDATORY
```php
// ALWAYS log ALL moderation actions
public function issueWarning(int $userId, string $reason, ?int $postId = null): int
{
    $warningId = Warnings::issue(
        $userId,
        Warnings::TYPE_WARNING,
        $reason,
        null,
        $postId,
        get_current_user_id(),
        $moderatorNotes
    );
    
    if ($warningId) {
        // Audit log - REQUIRED
        do_action('presszone_forum_warning_issued', $warningId, $userId, $reason, get_current_user_id());
        
        // Additional logging for sensitive actions
        error_log(sprintf(
            'Moderation: User %d issued warning to user %d. Reason: %s. Warning ID: %d',
            get_current_user_id(),
            $userId,
            $reason,
            $warningId
        ));
    }
    
    return $warningId;
}
```

#### Content Validation Security
```php
// ALWAYS use PostCreator methods - NEVER duplicate validation logic
$creator = new PostCreator();
try {
    // Check external links (if restricted)
    $creator->checkExternalLinks($content);
    
    // Check word filter
    $creator->checkWordFilter($content);
    
    // Sanitize for storage
    $sanitized = $creator->sanitizeMessage($content);
} catch (\InvalidArgumentException $e) {
    // Log the violation attempt
    error_log(sprintf(
        'Content violation: User %d attempted to post filtered content: %s',
        get_current_user_id(),
        $e->getMessage()
    ));
    
    return $this->respondError('content_violation', $e->getMessage(), 400);
}

// FORBIDDEN - Duplicating validation logic inline
if (preg_match('/https?:\/\//', $content)) {  // NEVER - use checkExternalLinks()
    return $this->respondError('external_links', 'External links not allowed', 400);
}
```

#### Nonce Verification
```php
// ALWAYS verify nonces for AJAX actions
if (!check_ajax_referer('presszone_forum_moderation', 'nonce', false)) {
    wp_send_json_error(['message' => __('Security check failed.', 'forum-press-zone')], 403);
}

// REST API nonces handled automatically via X-WP-Nonce header
// Just ensure permission_callback is set
'permission_callback' => [$this, 'checkModeratorPermission'],
```

### Data Protection & Privacy

#### Personal Data Handling
```php
// NEVER log personal information
error_log('Banned user: ' . $user->user_email);  // FORBIDDEN - contains PII

// CORRECT - Log only IDs and actions
error_log(sprintf(
    'User %d banned by moderator %d. Duration: %s. Reason: %s',
    $userId,
    get_current_user_id(),
    $duration,
    'Policy violation'  // Generic reason, not full text
));
```

#### GDPR Compliance
```php
// ALWAYS provide data export for moderation history
public function exportUserModerationData(int $userId): array
{
    $data = [];
    
    // Warnings
    $warnings = Warnings::getWarnings($userId);
    $data['warnings'] = array_map(function($warning) {
        return [
            'type' => $warning['warning_type'],
            'reason' => $warning['reason'],
            'date' => $warning['created_at'],
            'expires' => $warning['expires_at'],
            'active' => (bool) $warning['is_active']
        ];
    }, $warnings);
    
    // Ban history
    $banInfo = Bans::getBanInfo($userId);
    if ($banInfo) {
        $data['ban_history'] = [
            'reason' => $banInfo['reason'],
            'banned_at' => $banInfo['banned_at'],
            'expires_at' => $banInfo['expires_at'],
            'is_active' => Bans::isBanned($userId)
        ];
    }
    
    return $data;
}

// ALWAYS provide data deletion
public function deleteUserModerationData(int $userId): void
{
    // Clear warnings (keep for audit, but mark as deleted)
    $wpdb->update(
        $this->prefix . 'warnings',
        [
            'is_active' => 0,
            'reason' => '[DELETED]',
            'moderator_notes' => '[DELETED]'
        ],
        ['user_id' => $userId],
        ['%d', '%s', '%s'],
        ['%d']
    );
    
    // Remove ban
    Bans::unban($userId);
}
```

### Content Moderation Security

#### Report Queue Security
```php
// ALWAYS validate report content access
public function resolveReport(int $reportId, string $action): bool
{
    $report = $this->getReport($reportId);
    if (!$report) {
        return false;
    }
    
    // Check if moderator can access this content's forum
    if (!Roles::canModerateAll()) {
        $contentNodeId = $this->getContentNodeId($report['content_type'], $report['content_id']);
        
        $moderators = new Moderators();
        if (!$moderators->canModerateNode(get_current_user_id(), $contentNodeId)) {
            return false;
        }
    }
    
    // Process resolution
    return $this->processReportResolution($reportId, $action);
}
```

#### Soft Delete Security
```php
// ALWAYS use soft deletes with audit trail
public function deleteContent(int $contentId, string $contentType, string $reason): bool
{
    $moderatorId = get_current_user_id();
    
    // Soft delete the content
    $result = $wpdb->update(
        $this->getTableForContentType($contentType),
        [
            'is_soft_deleted' => 1,
            'deleted_at' => current_time('mysql'),
            'deleted_by' => $moderatorId,
            'deletion_reason' => sanitize_text_field($reason)
        ],
        [$this->getIdColumnForContentType($contentType) => $contentId],
        ['%d', '%s', '%d', '%s'],
        ['%d']
    );
    
    if ($result) {
        // Audit log
        do_action('presszone_forum_content_deleted', $contentId, $contentType, $reason, $moderatorId);
        
        // Update user stats
        $this->updateUserStats($contentId, $contentType);
    }
    
    return (bool) $result;
}
```

### JavaScript Security (Admin UI)

#### XSS Prevention
```javascript
// ALWAYS escape HTML content
function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Use in templates
const html = `<span class="user-name">${escapeHtml(user.display_name)}</span>`;

// NEVER use innerHTML with API data
element.innerHTML = response.user_name;  // FORBIDDEN
element.textContent = response.user_name;  // CORRECT
```

#### API Request Security
```javascript
// ALWAYS include nonce in requests
async function issueWarning(userId, warningData) {
    const response = await fetch(CONFIG.restUrl + '/users/' + userId + '/warnings', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce': CONFIG.nonce  // CRITICAL
        },
        credentials: 'same-origin',
        body: JSON.stringify(warningData)
    });
    
    if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }
    
    return response.json();
}
```

#### Input Validation
```javascript
// ALWAYS validate user input before sending to API
function validateWarningForm(formData) {
    const errors = [];
    
    if (!formData.user_id || !Number.isInteger(formData.user_id) || formData.user_id <= 0) {
        errors.push('Invalid user ID');
    }
    
    if (!formData.reason || formData.reason.trim().length < 3) {
        errors.push('Reason must be at least 3 characters');
    }
    
    if (formData.reason && formData.reason.length > 500) {
        errors.push('Reason must be less than 500 characters');
    }
    
    if (formData.duration && (!Number.isInteger(formData.duration) || formData.duration < 0)) {
        errors.push('Invalid duration');
    }
    
    return errors;
}
```

### Performance Security

#### Rate Limiting
```php
// ALWAYS implement rate limiting for moderation actions
public function checkModerationRateLimit(int $moderatorId): bool
{
    $cacheKey = "mod_rate_limit_{$moderatorId}";
    $actions = wp_cache_get($cacheKey, 'presszone_forum_moderation') ?: 0;
    
    // Limit: 50 actions per hour
    if ($actions >= 50) {
        return false;
    }
    
    wp_cache_set($cacheKey, $actions + 1, 'presszone_forum_moderation', 3600);
    return true;
}

// Use before moderation actions
if (!$this->checkModerationRateLimit(get_current_user_id())) {
    return $this->respondError('rate_limit', 'Too many moderation actions. Please wait.', 429);
}
```

#### Query Optimization
```php
// ALWAYS limit query results to prevent DoS
public function getReports(int $page = 1, int $perPage = 50): array
{
    $perPage = min(100, max(1, $perPage)); // Cap at 100
    $offset = ($page - 1) * $perPage;
    
    return $wpdb->get_results(
        $wpdb->prepare(
            "SELECT r.*, u.display_name as reporter_name
             FROM {$this->prefix}reports r
             LEFT JOIN {$wpdb->users} u ON r.reporter_id = u.ID
             WHERE r.status = 'open'
             ORDER BY r.created_at DESC
             LIMIT %d OFFSET %d",
            $perPage,
            $offset
        ),
        ARRAY_A
    );
}
```

### Testing Security Requirements

#### Moderation Testing
```php
// ALWAYS test permission enforcement
class TestModerationSecurity extends WP_UnitTestCase
{
    public function test_cannot_ban_administrator()
    {
        $adminId = $this->factory->user->create(['role' => 'administrator']);
        $result = Bans::ban($adminId, 'permanent', 'Test');
        $this->assertFalse($result);
    }
    
    public function test_moderator_cannot_access_other_forums()
    {
        $moderatorId = $this->factory->user->create(['role' => 'forum_moderator']);
        $nodeId = 123;
        $otherNodeId = 456;
        
        // Assign to specific node
        $moderators = new Moderators();
        $moderators->assign($moderatorId, $nodeId, 1);
        
        wp_set_current_user($moderatorId);
        
        // Should be able to moderate assigned node
        $this->assertTrue($moderators->canModerateNode($moderatorId, $nodeId));
        
        // Should NOT be able to moderate other nodes
        $this->assertFalse($moderators->canModerateNode($moderatorId, $otherNodeId));
    }
    
    public function test_content_validation_prevents_violations()
    {
        $creator = new PostCreator();
        
        // Test external link blocking
        $this->expectException(\InvalidArgumentException::class);
        $creator->checkExternalLinks('Visit https://malicious-site.com for more info');
        
        // Test word filter
        $this->expectException(\InvalidArgumentException::class);
        $creator->checkWordFilter('This contains a banned word');
    }
}
```

#### Security Audit Testing
```php
public function test_audit_logging_works()
{
    $userId = $this->factory->user->create(['role' => 'subscriber']);
    $moderatorId = $this->factory->user->create(['role' => 'forum_moderator']);
    
    wp_set_current_user($moderatorId);
    
    // Capture action calls
    $actionsCalled = [];
    add_action('presszone_forum_warning_issued', function($warningId, $userId, $reason, $issuedBy) use (&$actionsCalled) {
        $actionsCalled[] = ['warning_issued', $warningId, $userId, $reason, $issuedBy];
    }, 10, 4);
    
    // Issue warning
    $warningId = Warnings::issue($userId, Warnings::TYPE_WARNING, 'Test reason', null, null, $moderatorId);
    
    // Verify audit action was called
    $this->assertNotEmpty($actionsCalled);
    $this->assertEquals('warning_issued', $actionsCalled[0][0]);
    $this->assertEquals($warningId, $actionsCalled[0][1]);
}
```

---

## Testing Patterns

### Unit Tests Location

`tests/php/includes/`

```php
class TestBans extends WP_UnitTestCase {
    public function test_cannot_ban_administrator() {
        $adminId = $this->factory->user->create(['role' => 'administrator']);
        $result = Bans::ban($adminId, 'permanent', 'Test');
        $this->assertFalse($result);
    }

    public function test_expired_ban_auto_clears() {
        $userId = $this->factory->user->create(['role' => 'subscriber']);
        // Ban for 1 second
        Bans::ban($userId, 1, 'Test');
        sleep(2);
        $this->assertFalse(Bans::isBanned($userId));
    }
}
```

### Integration Test Pattern

```php
class TestModerationEndpoints extends WP_UnitTestCase {
    public function test_issue_warning_requires_moderator() {
        $request = new WP_REST_Request('POST', '/presszone-forum/v1/users/1/warnings');
        $request->set_param('type', 'warning');
        $request->set_param('reason', 'Test');

        // As regular user
        wp_set_current_user($this->subscriber_id);
        $response = rest_do_request($request);
        $this->assertEquals(403, $response->get_status());

        // As moderator
        wp_set_current_user($this->moderator_id);
        $response = rest_do_request($request);
        $this->assertEquals(201, $response->get_status());
    }
}
```