# AJAX Development Skill

> **Technology:** WordPress AJAX API for asynchronous requests

---

## Purpose

This skill covers WordPress AJAX patterns, action registration, and frontend communication for the Comments Press Zone plugin.

---

## AJAX Action Naming

**Format:** `presszone_comments_{action}`

**Examples:**
- `presszone_comments_submit`
- `presszone_comments_vote`
- `presszone_comments_report`
- `presszone_comments_edit`

---

## Backend: Action Registration

```php
namespace CommentsPressZone\Comments;

class Actions {
    public function register_hooks(): void {
        // Logged-in users
        add_action('wp_ajax_presszone_comments_vote', [$this, 'handle_vote']);
        add_action('wp_ajax_presszone_comments_report', [$this, 'handle_report']);
        
        // Non-logged-in users (public actions)
        add_action('wp_ajax_nopriv_presszone_comments_submit', [$this, 'handle_submit']);
        add_action('wp_ajax_nopriv_presszone_comments_vote', [$this, 'handle_vote_public']);
    }
}
```

---

## Backend: Handler Pattern

```php
public function handle_vote(): void {
    // 1. Verify nonce
    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'presszone_comments_nonce')) {
        wp_send_json_error([
            'message' => esc_html__('Security check failed.', 'comments-press-zone'),
        ]);
    }
    
    // 2. Check authentication
    if (!is_user_logged_in()) {
        wp_send_json_error([
            'message' => esc_html__('You must be logged in.', 'comments-press-zone'),
        ]);
    }
    
    // 3. Sanitize input
    $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
    $type = isset($_POST['type']) ? sanitize_key($_POST['type']) : '';
    
    // 4. Validate
    if ($comment_id === 0 || !in_array($type, ['upvote', 'downvote'], true)) {
        wp_send_json_error([
            'message' => esc_html__('Invalid data.', 'comments-press-zone'),
        ]);
    }
    
    // 5. Process
    $result = $this->process_vote($comment_id, $type);
    
    // 6. Respond
    wp_send_json_success([
        'action' => $result['action'],
        'upvotes' => $result['upvotes'],
        'downvotes' => $result['downvotes'],
    ]);
}
```

---

## Frontend: Localized Data

```php
public function enqueue_frontend_assets(): void {
    wp_enqueue_script(
        'presszone-comments-frontend',
        PRESSZONE_COMMENTS_URL . 'assets/js/frontend.js',
        [],
        PRESSZONE_COMMENTS_VERSION,
        true
    );
    
    wp_localize_script(
        'presszone-comments-frontend',
        'presszoneCommentsData',
        [
            'ajaxUrl' => admin_url('admin-ajax.php'),
            'nonce' => wp_create_nonce('presszone_comments_nonce'),
            'userId' => get_current_user_id(),
            'i18n' => [
                'confirm' => esc_html__('Are you sure?', 'comments-press-zone'),
                'error' => esc_html__('An error occurred.', 'comments-press-zone'),
            ],
        ]
    );
}
```

---

## Frontend: AJAX Request

```javascript
async function handleVote(commentId, type) {
    const formData = new FormData();
    formData.append('action', 'presszone_comments_vote');
    formData.append('nonce', window.presszoneCommentsData.nonce);
    formData.append('comment_id', commentId);
    formData.append('type', type);
    
    try {
        const response = await fetch(window.presszoneCommentsData.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });
        
        const result = await response.json();
        
        if (result.success) {
            // Handle success
            updateVoteUI(commentId, result.data);
            showToast('Vote recorded', 'success');
        } else {
            // Handle error
            showToast(result.data.message, 'error');
        }
    } catch (error) {
        console.error('AJAX Error:', error);
        showToast('Network error', 'error');
    }
}
```

---

## Response Patterns

### Success Response

```php
wp_send_json_success([
    'message' => esc_html__('Operation successful.', 'comments-press-zone'),
    'data' => [
        'id' => 123,
        'status' => 'active',
    ],
]);
```

**Output:**
```json
{
    "success": true,
    "data": {
        "message": "Operation successful.",
        "data": {
            "id": 123,
            "status": "active"
        }
    }
}
```

### Error Response

```php
wp_send_json_error([
    'message' => esc_html__('Validation failed.', 'comments-press-zone'),
    'code' => 'validation_error',
]);
```

**Output:**
```json
{
    "success": false,
    "data": {
        "message": "Validation failed.",
        "code": "validation_error"
    }
}
```

---

## Common AJAX Actions

### Submit Comment

```php
public function handle_submit(): void {
    // Verify nonce
    check_ajax_referer('presszone_comments_nonce', 'nonce');
    
    // Sanitize
    $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
    $content = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    $parent = isset($_POST['parent']) ? absint($_POST['parent']) : 0;
    
    // Validate
    if (empty($content)) {
        wp_send_json_error(['message' => esc_html__('Comment content required.', 'comments-press-zone')]);
    }
    
    // Insert comment
    $comment_id = wp_insert_comment([
        'comment_post_ID' => $post_id,
        'comment_content' => $content,
        'comment_parent' => $parent,
        'user_id' => get_current_user_id(),
    ]);
    
    if (is_wp_error($comment_id)) {
        wp_send_json_error(['message' => $comment_id->get_error_message()]);
    }
    
    // Get rendered comment HTML
    ob_start();
    $this->render_comment($comment_id);
    $html = ob_get_clean();
    
    wp_send_json_success([
        'message' => esc_html__('Comment submitted.', 'comments-press-zone'),
        'comment_id' => $comment_id,
        'html' => $html,
    ]);
}
```

### Edit Comment

```php
public function handle_edit(): void {
    check_ajax_referer('presszone_comments_nonce', 'nonce');
    
    $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
    $content = isset($_POST['content']) ? wp_kses_post(wp_unslash($_POST['content'])) : '';
    
    // Check permission
    $comment = get_comment($comment_id);
    if (!$comment || !current_user_can('edit_comment', $comment_id)) {
        wp_send_json_error(['message' => esc_html__('Unauthorized.', 'comments-press-zone')]);
    }
    
    // Update
    wp_update_comment([
        'comment_ID' => $comment_id,
        'comment_content' => $content,
    ]);
    
    wp_send_json_success([
        'message' => esc_html__('Comment updated.', 'comments-press-zone'),
    ]);
}
```

### Report Comment

```php
public function handle_report(): void {
    check_ajax_referer('presszone_comments_nonce', 'nonce');
    
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => esc_html__('Login required.', 'comments-press-zone')]);
    }
    
    $comment_id = isset($_POST['comment_id']) ? absint($_POST['comment_id']) : 0;
    $reason = isset($_POST['reason']) ? sanitize_textarea_field(wp_unslash($_POST['reason'])) : '';
    
    global $wpdb;
    $table = $wpdb->prefix . 'presszone_comments_reports';
    
    $wpdb->insert($table, [
        'comment_id' => $comment_id,
        'user_id' => get_current_user_id(),
        'reason' => $reason,
        'status' => 'open',
    ], ['%d', '%d', '%s', '%s']);
    
    wp_send_json_success([
        'message' => esc_html__('Thank you for your report.', 'comments-press-zone'),
    ]);
}
```

---

## Security Checklist

### Backend (PHP)

- [ ] Nonce verified with `check_ajax_referer()` or `wp_verify_nonce()`
- [ ] User authentication checked (`is_user_logged_in()`)
- [ ] Permissions checked (`current_user_can()`)
- [ ] All input sanitized (`sanitize_*`, `absint`, etc.)
- [ ] All output escaped (`esc_html__`, etc.)
- [ ] Use `wp_unslash()` before sanitizing `$_POST`

### Frontend (JS)

- [ ] Nonce included in request
- [ ] `credentials: 'same-origin'` in fetch
- [ ] Error handling implemented
- [ ] No `innerHTML` with response data
- [ ] User feedback provided (toast/message)

---

## Common Mistakes to Avoid

| Mistake | Fix |
|---------|-----|
| Missing nonce verification | Use `check_ajax_referer()` first |
| No `wp_unslash()` before sanitizing | Always unslash `$_POST` data |
| Using `die()` instead of `wp_send_json_*` | Use WordPress functions for consistency |
| Not checking user permissions | Verify capabilities before processing |
| Missing `credentials: 'same-origin'` | Required for cookies in fetch |
| Not registering `nopriv` action | Add for public endpoints |

---

## Testing Checklist

- [ ] Nonce verification works
- [ ] Permission checks enforced
- [ ] Input validation catches bad data
- [ ] Success/error responses consistent
- [ ] Frontend handles both success and error
- [ ] Network errors handled gracefully
- [ ] All text strings translatable
