# AJAX Skill

> **Purpose:** WordPress AJAX patterns, action handlers, and AJAX security
> **When to use:** Any task involving WordPress admin-ajax.php requests
> **Related skills:** wordpress-plugin-foundation-skill.md (always applies)

---

## Quick Reference

```php
// Register AJAX action
add_action('wp_ajax_presszone_forum_action', [$this, 'handleAction']);
add_action('wp_ajax_nopriv_presszone_forum_action', [$this, 'handlePublicAction']);

// Handler
public function handleAction(): void
{
    // Verify nonce
    if (!check_ajax_referer('presszone_forum_action', 'nonce', false)) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    // Check permission
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Login required'], 401);
    }
    
    // Sanitize input
    $post_id = absint($_POST['post_id']);
    
    // Process...
    
    // Success response
    wp_send_json_success(['data' => $result]);
}
```

```javascript
// Client-side request
const formData = new FormData();
formData.append('action', 'presszone_forum_action');
formData.append('nonce', presszoneForumData.nonce);
formData.append('post_id', postId);

const response = await fetch(presszoneForumData.ajaxUrl, {
    method: 'POST',
    body: formData,
    credentials: 'same-origin'
});

const result = await response.json();
if (result.success) {
    console.log(result.data);
}
```

---

## Action Registration

### Authenticated Actions

```php
// Only for logged-in users
add_action('wp_ajax_presszone_forum_action', [$this, 'handleAction']);

public function handleAction(): void
{
    // User is guaranteed to be logged in
    $user_id = get_current_user_id();
    
    // Process action...
}
```

### Public Actions

```php
// For non-logged-in users
add_action('wp_ajax_nopriv_presszone_forum_action', [$this, 'handlePublicAction']);

public function handlePublicAction(): void
{
    // No user logged in
    // Still verify nonce!
    
    // Process action...
}
```

### Both Authenticated and Public

```php
// Same handler for both
add_action('wp_ajax_presszone_forum_action', [$this, 'handleAction']);
add_action('wp_ajax_nopriv_presszone_forum_action', [$this, 'handleAction']);

public function handleAction(): void
{
    // Check if user is logged in
    if (is_user_logged_in()) {
        // Authenticated logic
    } else {
        // Public logic
    }
}
```

---

## Security (CRITICAL)

### Nonce Verification (MANDATORY)

```php
public function handleAction(): void
{
    // ALWAYS verify nonce first
    if (!check_ajax_referer('presszone_forum_action', 'nonce', false)) {
        wp_send_json_error([
            'message' => __('Security check failed', 'forum-press-zone')
        ], 403);
    }
    
    // Continue processing...
}
```

### Permission Checks

```php
public function handleModerateAction(): void
{
    // Verify nonce
    if (!check_ajax_referer('presszone_forum_moderate', 'nonce', false)) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    
    // Check login
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Login required'], 401);
    }
    
    // Check permission
    if (!Roles::canModerate()) {
        wp_send_json_error(['message' => 'Access denied'], 403);
    }
    
    // Process moderation action...
}
```

### Input Sanitization

```php
public function handleAction(): void
{
    // Verify nonce first
    check_ajax_referer('presszone_forum_action', 'nonce');
    
    // Sanitize all input
    $post_id = absint($_POST['post_id']);
    $title = sanitize_text_field(wp_unslash($_POST['title']));
    $content = wp_kses_post(wp_unslash($_POST['content']));
    $tags = array_map('sanitize_text_field', $_POST['tags'] ?? []);
    
    // Validate
    if (!$post_id || $post_id <= 0) {
        wp_send_json_error(['message' => 'Invalid post ID'], 400);
    }
    
    // Process...
}
```

---

## Response Handling

### Success Response

```php
// Simple success
wp_send_json_success();

// Success with data
wp_send_json_success([
    'message' => __('Post created', 'forum-press-zone'),
    'post_id' => $post_id,
]);

// Success with custom status code
wp_send_json_success(['data' => $result], 201);
```

### Error Response

```php
// Simple error
wp_send_json_error();

// Error with message
wp_send_json_error([
    'message' => __('Post not found', 'forum-press-zone'),
]);

// Error with custom status code
wp_send_json_error([
    'message' => __('Access denied', 'forum-press-zone'),
], 403);
```

### Response Format

```php
// Success response structure
{
    "success": true,
    "data": {
        "message": "Post created",
        "post_id": 123
    }
}

// Error response structure
{
    "success": false,
    "data": {
        "message": "Post not found"
    }
}
```

---

## Client-Side Requests

### Using Fetch API

```javascript
async function ajaxRequest(action, data) {
    const CONFIG = window.presszoneForumData || {};
    
    const formData = new FormData();
    formData.append('action', action);
    formData.append('nonce', CONFIG.nonce);
    
    // Add data
    Object.keys(data).forEach(key => {
        if (Array.isArray(data[key])) {
            data[key].forEach(value => {
                formData.append(key + '[]', value);
            });
        } else {
            formData.append(key, data[key]);
        }
    });
    
    try {
        const response = await fetch(CONFIG.ajaxUrl, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin'
        });
        
        const result = await response.json();
        
        if (!result.success) {
            throw new Error(result.data?.message || 'Request failed');
        }
        
        return result.data;
    } catch (error) {
        console.error('AJAX request failed:', error);
        throw error;
    }
}

// Usage
try {
    const result = await ajaxRequest('presszone_forum_react', {
        post_id: 123,
        reaction_type: 'like'
    });
    console.log('Success:', result);
} catch (error) {
    showError(error.message);
}
```

### Using jQuery (Legacy)

```javascript
jQuery.ajax({
    url: presszoneForumData.ajaxUrl,
    type: 'POST',
    data: {
        action: 'presszone_forum_action',
        nonce: presszoneForumData.nonce,
        post_id: 123
    },
    success: function(response) {
        if (response.success) {
            console.log(response.data);
        } else {
            console.error(response.data.message);
        }
    },
    error: function(xhr, status, error) {
        console.error('AJAX error:', error);
    }
});
```

---

## Localize Script (Pass Data to JS)

### PHP Side

```php
public function enqueueScripts(): void
{
    wp_enqueue_script(
        'presszone-forum-frontend',
        plugins_url('assets/js/frontend.js', __FILE__),
        [],
        PRESSZONE_FORUM_VERSION,
        true
    );
    
    wp_localize_script('presszone-forum-frontend', 'presszoneForumData', [
        'ajaxUrl' => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('presszone_forum_action'),
        'strings' => [
            'confirmDelete' => __('Are you sure?', 'forum-press-zone'),
            'error' => __('An error occurred', 'forum-press-zone'),
        ],
        'settings' => [
            'postsPerPage' => get_option('presszone_forum_posts_per_page', 50),
        ],
    ]);
}
```

### JavaScript Side

```javascript
// Access localized data
const CONFIG = window.presszoneForumData || {};
const ajaxUrl = CONFIG.ajaxUrl;
const nonce = CONFIG.nonce;
const strings = CONFIG.strings || {};
```

---

## Common Patterns

### Toggle Action (Like/Unlike)

```php
public function handleToggleReaction(): void
{
    check_ajax_referer('presszone_forum_reaction', 'nonce');
    
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Login required'], 401);
    }
    
    $post_id = absint($_POST['post_id']);
    $reaction_type = sanitize_key($_POST['reaction_type'] ?? 'like');
    $user_id = get_current_user_id();
    
    // Check if already reacted
    $existing = $this->getReaction($post_id, $user_id);
    
    if ($existing) {
        // Remove reaction
        $this->removeReaction($post_id, $user_id);
        $action = 'removed';
    } else {
        // Add reaction
        $this->addReaction($post_id, $user_id, $reaction_type);
        $action = 'added';
    }
    
    wp_send_json_success([
        'action' => $action,
        'reactions' => $this->getPostReactions($post_id),
    ]);
}
```

### Form Submission

```php
public function handleFormSubmit(): void
{
    check_ajax_referer('presszone_forum_submit', 'nonce');
    
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Login required'], 401);
    }
    
    // Sanitize input
    $title = sanitize_text_field(wp_unslash($_POST['title']));
    $content = wp_kses_post(wp_unslash($_POST['content']));
    
    // Validate
    if (empty($title) || strlen($title) < 3) {
        wp_send_json_error([
            'message' => __('Title must be at least 3 characters', 'forum-press-zone'),
        ], 400);
    }
    
    // Create post
    $post_id = $this->createPost([
        'title' => $title,
        'content' => $content,
        'user_id' => get_current_user_id(),
    ]);
    
    if (!$post_id) {
        wp_send_json_error([
            'message' => __('Failed to create post', 'forum-press-zone'),
        ], 500);
    }
    
    wp_send_json_success([
        'message' => __('Post created successfully', 'forum-press-zone'),
        'post_id' => $post_id,
        'redirect' => $this->getPostUrl($post_id),
    ]);
}
```

### Search/Autocomplete

```php
public function handleSearch(): void
{
    check_ajax_referer('presszone_forum_search', 'nonce');
    
    $query = sanitize_text_field(wp_unslash($_POST['query'] ?? ''));
    
    if (strlen($query) < 2) {
        wp_send_json_success(['results' => []]);
    }
    
    $results = $this->searchPosts($query, 10);
    
    wp_send_json_success(['results' => $results]);
}
```

---

## File Uploads via AJAX

### PHP Handler

```php
public function handleFileUpload(): void
{
    check_ajax_referer('presszone_forum_upload', 'nonce');
    
    if (!is_user_logged_in()) {
        wp_send_json_error(['message' => 'Login required'], 401);
    }
    
    if (empty($_FILES['file'])) {
        wp_send_json_error(['message' => 'No file uploaded'], 400);
    }
    
    // Use WordPress upload handler
    $file = wp_handle_upload($_FILES['file'], [
        'test_form' => false,
        'mimes' => [
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'gif' => 'image/gif',
        ],
    ]);
    
    if (isset($file['error'])) {
        wp_send_json_error(['message' => $file['error']], 400);
    }
    
    wp_send_json_success([
        'url' => $file['url'],
        'filename' => basename($file['file']),
    ]);
}
```

### JavaScript Client

```javascript
async function uploadFile(file) {
    const formData = new FormData();
    formData.append('action', 'presszone_forum_upload');
    formData.append('nonce', presszoneForumData.nonce);
    formData.append('file', file);
    
    const response = await fetch(presszoneForumData.ajaxUrl, {
        method: 'POST',
        body: formData,
        credentials: 'same-origin'
    });
    
    const result = await response.json();
    
    if (!result.success) {
        throw new Error(result.data.message);
    }
    
    return result.data;
}

// Usage
fileInput.addEventListener('change', async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    
    try {
        const result = await uploadFile(file);
        console.log('Uploaded:', result.url);
    } catch (error) {
        showError(error.message);
    }
});
```

---

## Rate Limiting

### Implement Rate Limiting

```php
public function handleAction(): void
{
    check_ajax_referer('presszone_forum_action', 'nonce');
    
    // Rate limiting
    $user_id = get_current_user_id();
    $cache_key = "rate_limit_action_{$user_id}";
    $count = wp_cache_get($cache_key, 'presszone_forum') ?: 0;
    
    if ($count >= 100) {  // 100 requests per hour
        wp_send_json_error([
            'message' => __('Too many requests. Please try again later.', 'forum-press-zone'),
        ], 429);
    }
    
    wp_cache_set($cache_key, $count + 1, 'presszone_forum', 3600);
    
    // Process action...
}
```

---

## Error Handling

### Try-Catch Pattern

```php
public function handleAction(): void
{
    check_ajax_referer('presszone_forum_action', 'nonce');
    
    try {
        $result = $this->processAction($_POST);
        
        wp_send_json_success([
            'message' => __('Action completed', 'forum-press-zone'),
            'data' => $result,
        ]);
    } catch (\InvalidArgumentException $e) {
        wp_send_json_error([
            'message' => $e->getMessage(),
        ], 400);
    } catch (\Exception $e) {
        error_log('AJAX action failed: ' . $e->getMessage());
        
        wp_send_json_error([
            'message' => __('An error occurred', 'forum-press-zone'),
        ], 500);
    }
}
```

---

## Debugging

### Enable AJAX Debugging

```php
// In wp-config.php
define('DOING_AJAX', true);

// Log AJAX requests
add_action('wp_ajax_presszone_forum_action', function() {
    error_log('AJAX action called: presszone_forum_action');
    error_log('POST data: ' . print_r($_POST, true));
}, 1);
```

### Client-Side Debugging

```javascript
// Log all AJAX requests
const originalFetch = window.fetch;
window.fetch = function(...args) {
    console.log('Fetch request:', args);
    return originalFetch.apply(this, args).then(response => {
        console.log('Fetch response:', response);
        return response;
    });
};
```

---

## Common Mistakes

| Mistake | Fix |
|---------|-----|
| Missing nonce verification | Always use `check_ajax_referer()` |
| Not checking login status | Use `is_user_logged_in()` |
| Missing input sanitization | Sanitize all `$_POST` data |
| Not handling errors | Use try-catch and return errors |
| Forgetting `wp_die()` at end | Use `wp_send_json_*()` which calls `wp_die()` |
| Using `echo` instead of `wp_send_json_*()` | Always use `wp_send_json_success/error()` |
| Not setting HTTP status codes | Pass status code to `wp_send_json_*()` |
| Missing `credentials: 'same-origin'` | Required for cookies/nonces |
| Not localizing AJAX URL | Use `wp_localize_script()` |
| Hardcoding action names | Use constants or variables |

---

## Integration with Other Skills

- **wordpress-plugin-foundation-skill.md** - Security and compliance (always applies)
- **php-skill.md** - PHP patterns and error handling
- **javascript-skill.md** - Client-side AJAX requests
- **rest-api-skill.md** - Alternative to AJAX (prefer REST for new code)
