# WordPress Plugin Foundation Skill

> **Purpose:** Core security, compliance, validation rules for ALL WordPress plugin dev
> **When to use:** ALWAYS — auto-applied every task
> **Critical:** Every rule mandatory, no skips

---

## Quick Reference - Security Essentials

```php
// Input Sanitization (ALWAYS)
$text = sanitize_text_field(wp_unslash($_POST['field']));
$html = wp_kses_post(wp_unslash($_POST['content']));
$id = absint($_POST['id']);
$email = sanitize_email($_POST['email']);

// Output Escaping (ALWAYS)
echo esc_html($text);           // HTML content
echo esc_attr($value);          // HTML attributes
echo esc_url($url);             // URLs
echo esc_js($script);           // JavaScript

// Nonce Verification (ALWAYS for forms/AJAX)
check_ajax_referer('action_name', 'nonce');
wp_verify_nonce($_POST['_wpnonce'], 'action_name');

// Permission Checks (ALWAYS for sensitive actions)
if (!current_user_can('manage_options')) {
    wp_die('Access denied');
}

// SQL Queries (ALWAYS use prepare)
$wpdb->query($wpdb->prepare("SELECT * FROM table WHERE id = %d", $id));
```

---

## WordPress.org Compliance Requirements (ZERO TOLERANCE)

### 1. Naming Conventions - 4+ Character Prefixes REQUIRED

**Rule:** ALL functions, classes, constants, globals, DB options MUST use unique 4+ char prefixes.

```php
// CORRECT - WordPress.org compliant
function presszone_forum_init() {}
class Presszone_Forum_Query {}
const PRESSZONE_FORUM_VERSION = '1.0';
$presszone_forum_data = [];
update_option('presszone_forum_settings', $data);

// FORBIDDEN - Will cause plugin rejection
function pz_init() {}        // 2 letters - TOO SHORT
function fpz_init() {}       // 3 letters - TOO SHORT
class FPZ_Query {}           // 3 letters - TOO SHORT
const PZ_VERSION = '1.0';    // 2 letters - TOO SHORT
```

**JavaScript Globals:**
```javascript
// CORRECT - 4+ characters
window.PresszoneForumApp = {};
window.presszoneForumData = {};
const PresszoneForumMessenger = {};

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

**Database Tables:**
```php
// CORRECT
$wpdb->prefix . 'presszone_forum_posts'

// FORBIDDEN
$wpdb->prefix . 'pz_posts'   // TOO SHORT
$wpdb->prefix . 'fpz_posts'  // TOO SHORT
```

### 2. Text Domain - MUST Match Plugin Slug

```php
// Plugin slug: forum-press-zone
// Text domain MUST be: 'forum-press-zone'

// CORRECT
__('Hello', 'forum-press-zone')
esc_html__('Hello', 'forum-press-zone')
_e('Hello', 'forum-press-zone')

// FORBIDDEN
__('Hello')                      // Missing text domain
__('Hello', 'presszone-forum')   // Wrong text domain
__('Hello', 'fpz')               // Wrong text domain
```

### 3. No Inline CSS (ABSOLUTE BAN)

```php
// FORBIDDEN - Will cause rejection
wp_add_inline_style('handle', $css);
echo '<style>.class { color: red; }</style>';
echo '<div style="margin: 10px;">';

// CORRECT - All styles in static files
// Write to: assets/css/*.css or wp-content/uploads/presszone-forum/custom.css
wp_enqueue_style('presszone-forum-styles', $url);
```

### 4. No CDN Resources

```php
// FORBIDDEN - External resources
wp_enqueue_script('jquery', 'https://code.jquery.com/jquery.min.js');
wp_enqueue_style('fonts', 'https://fonts.googleapis.com/css?family=Roboto');

// CORRECT - Bundle locally
wp_enqueue_script('jquery', plugins_url('assets/js/jquery.min.js', __FILE__));
wp_enqueue_style('fonts', plugins_url('assets/fonts/roboto.css', __FILE__));
```

### 5. Direct File Access Protection

```php
// REQUIRED at top of EVERY PHP file
<?php
declare(strict_types=1);

namespace PresszoneForumPlugin;

if (!defined('ABSPATH')) {
    exit;
}
```

### 6. Proper Enqueueing with Hook Checks

```php
// FORBIDDEN - Global enqueueing
add_action('admin_enqueue_scripts', function() {
    wp_enqueue_script('my-script', $url);  // Loads on ALL admin pages
});

// CORRECT - Check hook suffix
add_action('admin_enqueue_scripts', function($hook) {
    if ($hook !== 'toplevel_page_presszone-forum') {
        return;
    }
    wp_enqueue_script('presszone-forum-admin', $url);
});
```

---

## Security Requirements (CRITICAL)

### SQL Injection Prevention

**Rule:** NEVER concat vars into SQL. ALWAYS use `$wpdb->prepare()`.

```php
// FORBIDDEN - SQL Injection vulnerability
$wpdb->query("SELECT * FROM table WHERE id = $id");
$wpdb->query("SELECT * FROM table WHERE name = '$name'");
$wpdb->get_results("SELECT * FROM table WHERE id = " . $id);

// CORRECT - Use prepare with placeholders
$wpdb->query($wpdb->prepare(
    "SELECT * FROM table WHERE id = %d",
    $id
));

$wpdb->query($wpdb->prepare(
    "SELECT * FROM table WHERE name = %s AND status = %s",
    $name,
    $status
));
```

**Special Cases:**

```php
// ORDER BY - Cannot use prepare for column names
// Use whitelist instead
$allowed_columns = ['date', 'title', 'author'];
$orderby = in_array($_GET['orderby'], $allowed_columns, true) 
    ? $_GET['orderby'] 
    : 'date';
$sql = "SELECT * FROM table ORDER BY {$orderby}";

// LIKE queries - Use esc_like()
$search = $wpdb->esc_like($_GET['search']);
$wpdb->prepare(
    "SELECT * FROM table WHERE title LIKE %s",
    '%' . $search . '%'
);

// IN clauses - Sanitize array
$ids = array_map('absint', $_POST['ids']);
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$wpdb->prepare(
    "SELECT * FROM table WHERE id IN ($placeholders)",
    ...$ids
);
```

### Cross-Site Scripting (XSS) Prevention

**Rule:** ALWAYS escape output at display point. Use correct escape function for context.

```php
// HTML Content
echo esc_html($text);
echo esc_html__('Translated text', 'forum-press-zone');

// HTML Attributes
echo '<input value="' . esc_attr($value) . '">';
echo '<div class="' . esc_attr($class) . '">';

// URLs
echo '<a href="' . esc_url($url) . '">';
echo '<img src="' . esc_url($image) . '">';

// JavaScript
echo '<script>var data = ' . wp_json_encode($data) . ';</script>';
echo '<script>alert("' . esc_js($message) . '");</script>';

// Rich HTML (allow specific tags)
echo wp_kses_post($content);  // Allows post content tags
echo wp_kses($content, [
    'a' => ['href' => [], 'title' => []],
    'strong' => [],
    'em' => []
]);
```

**JavaScript XSS Prevention:**

```javascript
// FORBIDDEN - XSS vulnerability
element.innerHTML = userInput;
element.innerHTML = apiResponse.message;

// CORRECT - Use textContent
element.textContent = userInput;

// CORRECT - Escape HTML
function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}
const html = `<span>${escapeHtml(user.name)}</span>`;
```

### Cross-Site Request Forgery (CSRF) Prevention

**Rule:** ALWAYS verify nonces for state-changing ops.

```php
// AJAX - Verify nonce
add_action('wp_ajax_presszone_forum_action', function() {
    if (!check_ajax_referer('presszone_forum_action', 'nonce', false)) {
        wp_send_json_error(['message' => 'Security check failed'], 403);
    }
    // Process action...
});

// Forms - Create and verify nonce
// In form:
wp_nonce_field('presszone_forum_action', '_wpnonce');

// In handler:
if (!wp_verify_nonce($_POST['_wpnonce'], 'presszone_forum_action')) {
    wp_die('Security check failed');
}

// REST API - Automatic via X-WP-Nonce header
// Just set permission_callback
register_rest_route('presszone-forum/v1', '/endpoint', [
    'methods' => 'POST',
    'callback' => [$this, 'handler'],
    'permission_callback' => [$this, 'checkPermission'],
]);
```

### Authentication vs Authorization

**Critical Rule:** `is_admin()` checks PAGE not USER. NOT security check.

```php
// FORBIDDEN - Authentication bypass vulnerability
if (is_admin()) {
    update_option('critical_setting', $_POST['value']);  // VULNERABLE!
}

// CORRECT - Check user capabilities
if (current_user_can('manage_options')) {
    update_option('critical_setting', sanitize_text_field($_POST['value']));
}

// CORRECT - Check specific permissions
if (Roles::canModerate()) {
    // Perform moderation action
}
```

**Permission Hierarchy:**

```php
// 1. Authentication - Is user logged in?
if (!is_user_logged_in()) {
    wp_send_json_error(['message' => 'Login required'], 401);
}

// 2. Authorization - Does user have permission?
if (!current_user_can('manage_options')) {
    wp_send_json_error(['message' => 'Access denied'], 403);
}

// 3. Ownership - Does user own this resource?
$post = get_post($post_id);
if ($post->post_author != get_current_user_id() && !current_user_can('edit_others_posts')) {
    wp_send_json_error(['message' => 'Cannot edit others posts'], 403);
}
```

### REST API Security

**Rule:** ALWAYS specify `permission_callback` for ALL endpoints.

```php
// FORBIDDEN - Open endpoint
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    // Missing permission_callback - VULNERABLE!
]);

// FORBIDDEN - Public write endpoint
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    'permission_callback' => '__return_true',  // VULNERABLE!
]);

// CORRECT - Proper permission check
register_rest_route('presszone-forum/v1', '/settings', [
    'methods' => 'POST',
    'callback' => [$this, 'updateSettings'],
    'permission_callback' => function() {
        return current_user_can('manage_options');
    },
]);
```

### File Upload Security

**Rule:** NEVER trust client MIME types. ALWAYS use WP upload handlers.

```php
// FORBIDDEN - Insecure upload
$filename = $_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'], $upload_dir . '/' . $filename);

// FORBIDDEN - Client MIME type check
if ($_FILES['file']['type'] === 'image/jpeg') {  // Easily spoofed!
    move_uploaded_file(...);
}

// CORRECT - Use WordPress upload handler
$file = wp_handle_upload($_FILES['file'], [
    'test_form' => false,
    'mimes' => [
        'jpg|jpeg|jpe' => 'image/jpeg',
        'png' => 'image/png',
    ],
]);

if (isset($file['error'])) {
    wp_send_json_error(['message' => $file['error']], 400);
}

$file_path = $file['file'];
$file_url = $file['url'];
```

### PHP Object Injection Prevention

**Rule:** NEVER use `unserialize()` on untrusted data.

```php
// FORBIDDEN - Object injection vulnerability
$data = unserialize($_POST['data']);
$data = unserialize($_COOKIE['session']);

// CORRECT - Use JSON
$data = json_decode($_POST['data'], true);

// If you MUST unserialize (rare), restrict classes
$data = unserialize($trusted_data, ['allowed_classes' => false]);
```

---

## Input Validation & Sanitization

### Validation Functions

```php
// Email
if (!is_email($email)) {
    return new WP_Error('invalid_email', 'Invalid email address');
}
$email = sanitize_email($email);

// Integer/ID
if (!is_numeric($id) || $id <= 0) {
    return new WP_Error('invalid_id', 'Invalid ID');
}
$id = absint($id);

// URL
if (!filter_var($url, FILTER_VALIDATE_URL)) {
    return new WP_Error('invalid_url', 'Invalid URL');
}
$url = esc_url_raw($url);

// File path (directory traversal check)
if (validate_file($path) !== 0) {
    return new WP_Error('invalid_path', 'Invalid file path');
}
```

### Sanitization by Type

```php
// Plain text (removes HTML tags)
$text = sanitize_text_field(wp_unslash($_POST['field']));

// Textarea (preserves line breaks)
$textarea = sanitize_textarea_field(wp_unslash($_POST['content']));

// Rich HTML (allows post content tags)
$html = wp_kses_post(wp_unslash($_POST['content']));

// Key/slug (lowercase alphanumeric + underscore)
$key = sanitize_key($_POST['key']);

// Title
$title = sanitize_title($_POST['title']);

// Filename
$filename = sanitize_file_name($_FILES['file']['name']);

// SQL ORDER BY
$orderby = sanitize_sql_orderby($_GET['orderby']);
```

### Array Sanitization

```php
// Sanitize array of IDs
$ids = array_map('absint', $_POST['ids'] ?? []);
$ids = array_filter($ids);  // Remove zeros

// Sanitize array of strings
$tags = array_map('sanitize_text_field', $_POST['tags'] ?? []);

// Deep sanitization
function sanitize_array_recursive($array) {
    foreach ($array as $key => $value) {
        if (is_array($value)) {
            $array[$key] = sanitize_array_recursive($value);
        } else {
            $array[$key] = sanitize_text_field($value);
        }
    }
    return $array;
}
```

---

## Permission & Access Control

### WordPress Capabilities

```php
// Admin check
if (!current_user_can('manage_options')) {
    return new WP_Error('forbidden', 'Access denied', ['status' => 403]);
}

// Editor check
if (!current_user_can('edit_posts')) {
    return new WP_Error('forbidden', 'Cannot edit posts', ['status' => 403]);
}

// Custom capability check
if (!current_user_can('presszone_forum_moderate')) {
    return new WP_Error('forbidden', 'Cannot moderate', ['status' => 403]);
}

// Check for specific user
if (!user_can($user_id, 'manage_options')) {
    return false;
}
```

### Ownership Validation (IDOR Prevention)

```php
// FORBIDDEN - Insecure Direct Object Reference
$post_id = absint($_POST['post_id']);
wp_delete_post($post_id);  // Any user can delete any post!

// CORRECT - Verify ownership
$post_id = absint($_POST['post_id']);
$post = get_post($post_id);

if (!$post) {
    return new WP_Error('not_found', 'Post not found', ['status' => 404]);
}

// Check if user owns the post OR has permission to edit others' posts
if ($post->post_author != get_current_user_id() && !current_user_can('edit_others_posts')) {
    return new WP_Error('forbidden', 'Cannot delete this post', ['status' => 403]);
}

wp_delete_post($post_id);
```

### Administrator Protection

```php
// ALWAYS protect administrators from moderation actions
if (user_can($target_user_id, 'administrator')) {
    return new WP_Error('forbidden', 'Cannot moderate administrators', ['status' => 403]);
}
```

---

## Common Security Vulnerabilities & Fixes

### 1. SQL Injection

```php
// VULNERABLE
$wpdb->query("DELETE FROM table WHERE id = {$_POST['id']}");

// FIXED
$wpdb->query($wpdb->prepare("DELETE FROM table WHERE id = %d", absint($_POST['id'])));
```

### 2. XSS (Reflected)

```php
// VULNERABLE
echo '<p>Search results for: ' . $_GET['q'] . '</p>';

// FIXED
echo '<p>Search results for: ' . esc_html($_GET['q']) . '</p>';
```

### 3. XSS (Stored)

```php
// VULNERABLE
$content = $_POST['content'];
$wpdb->insert('posts', ['content' => $content]);
// Later:
echo $row['content'];  // XSS!

// FIXED
$content = wp_kses_post(wp_unslash($_POST['content']));
$wpdb->insert('posts', ['content' => $content], ['%s']);
// Later:
echo wp_kses_post($row['content']);
```

### 4. CSRF

```php
// VULNERABLE
if ($_POST['action'] === 'delete') {
    wp_delete_post($_POST['id']);  // No nonce check!
}

// FIXED
if ($_POST['action'] === 'delete') {
    if (!wp_verify_nonce($_POST['_wpnonce'], 'delete_post')) {
        wp_die('Security check failed');
    }
    wp_delete_post(absint($_POST['id']));
}
```

### 5. Authentication Bypass

```php
// VULNERABLE
if (is_admin()) {  // Checks page, not user!
    update_option('setting', $_POST['value']);
}

// FIXED
if (current_user_can('manage_options')) {
    update_option('setting', sanitize_text_field($_POST['value']));
}
```

### 6. Privilege Escalation

```php
// VULNERABLE
add_action('wp_ajax_update_role', function() {
    $user = get_userdata($_POST['user_id']);
    $user->set_role($_POST['role']);  // Any logged-in user can change roles!
});

// FIXED
add_action('wp_ajax_update_role', function() {
    if (!current_user_can('promote_users')) {
        wp_send_json_error(['message' => 'Access denied'], 403);
    }
    
    $user_id = absint($_POST['user_id']);
    $role = sanitize_key($_POST['role']);
    
    // Validate role exists
    if (!get_role($role)) {
        wp_send_json_error(['message' => 'Invalid role'], 400);
    }
    
    $user = get_userdata($user_id);
    if ($user) {
        $user->set_role($role);
        wp_send_json_success();
    }
});
```

### 7. Open Redirect

```php
// VULNERABLE
wp_redirect($_GET['redirect_to']);

// FIXED
wp_safe_redirect($_GET['redirect_to']);  // Restricts to local domain
```

### 8. Server-Side Request Forgery (SSRF)

```php
// VULNERABLE
$response = wp_remote_get($_POST['url']);  // Can access internal network!

// FIXED
$response = wp_safe_remote_get($_POST['url']);  // Blocks private IPs
```

---

## WordPress Coding Standards

### Namespace & File Structure

```php
<?php
declare(strict_types=1);

namespace PresszoneForumPlugin;

if (!defined('ABSPATH')) {
    exit;
}

class MyClass
{
    // Class implementation
}
```

### File Naming

```
class-presszone-forum-query.php     → class Query
class-presszone-forum-post-creator.php → class PostCreator
```

### Constants

```php
// Plugin constants
define('PRESSZONE_FORUM_VERSION', '1.0.0');
define('PRESSZONE_FORUM_PATH', plugin_dir_path(__FILE__));

// Class constants
class Roles
{
    public const CAP_MODERATE = 'presszone_forum_moderate';
    public const ROLE_MODERATOR = 'forum_moderator';
}
```

---

## HTTP Request Security

### Remote Requests

```php
// FORBIDDEN - Direct curl/file_get_contents
$data = file_get_contents('https://api.example.com');
curl_exec($ch);

// CORRECT - WordPress HTTP API
$response = wp_remote_get('https://api.example.com', [
    'timeout' => 15,
    'headers' => [
        'Authorization' => 'Bearer ' . $token,
    ],
]);

if (is_wp_error($response)) {
    return $response;
}

$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
```

### Safe Remote Requests (SSRF Prevention)

```php
// Use wp_safe_remote_get() to block private IPs
$response = wp_safe_remote_get($url);
```

---

## Data Storage Security

### Options

```php
// ALWAYS sanitize before storing
update_option('presszone_forum_settings', [
    'title' => sanitize_text_field($title),
    'description' => sanitize_textarea_field($description),
    'enabled' => (bool) $enabled,
]);

// ALWAYS escape when displaying
$settings = get_option('presszone_forum_settings', []);
echo esc_html($settings['title']);
```

### User Meta

```php
// ALWAYS sanitize
update_user_meta($user_id, 'presszone_forum_signature', wp_kses_post($signature));

// ALWAYS escape
$signature = get_user_meta($user_id, 'presszone_forum_signature', true);
echo wp_kses_post($signature);
```

### Transients (Caching)

```php
// Set transient
set_transient('presszone_forum_stats', $stats, HOUR_IN_SECONDS);

// Get transient
$stats = get_transient('presszone_forum_stats');
if ($stats === false) {
    $stats = $this->calculateStats();
    set_transient('presszone_forum_stats', $stats, HOUR_IN_SECONDS);
}
```

---

## Error Handling & Logging

### Error Responses

```php
// AJAX
wp_send_json_error([
    'message' => __('Operation failed', 'forum-press-zone'),
    'code' => 'operation_failed',
], 400);

// REST API
return new WP_Error(
    'operation_failed',
    __('Operation failed', 'forum-press-zone'),
    ['status' => 400]
);
```

### Logging

```php
// NEVER log sensitive data
error_log('User email: ' . $user->user_email);  // FORBIDDEN - PII

// CORRECT - Log only IDs and actions
error_log(sprintf(
    'User %d performed action %s at %s',
    $user_id,
    $action,
    current_time('mysql')
));
```

---

## Performance & Security

### Rate Limiting

```php
// Implement rate limiting for sensitive actions
$cache_key = "rate_limit_{$action}_{$user_id}";
$count = wp_cache_get($cache_key, 'presszone_forum') ?: 0;

if ($count >= 100) {  // 100 actions per hour
    return new WP_Error('rate_limit', 'Too many requests', ['status' => 429]);
}

wp_cache_set($cache_key, $count + 1, 'presszone_forum', 3600);
```

### Query Limits

```php
// ALWAYS limit query results
$limit = min(100, max(1, absint($_GET['limit'] ?? 50)));
$offset = max(0, absint($_GET['offset'] ?? 0));

$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM table LIMIT %d OFFSET %d",
    $limit,
    $offset
));
```

---

## Common Mistakes (CRITICAL)

| Mistake | Fix |
|---------|-----|
| Short prefixes (`pz_`, `fpz_`) | Use 4+ char prefixes (`presszone_forum_`) |
| Missing text domain | Use `'forum-press-zone'` |
| `is_admin()` for security | Use `current_user_can()` |
| Concatenating SQL | Use `$wpdb->prepare()` |
| Missing output escaping | Use `esc_html()`, `esc_attr()`, `esc_url()` |
| Missing nonce verification | Use `check_ajax_referer()`, `wp_verify_nonce()` |
| `unserialize()` on user input | Use `json_decode()` |
| Trust `$_FILES['file']['type']` | Use `wp_handle_upload()` |
| Missing permission checks | Use `current_user_can()` |
| No ownership validation | Check `$post->post_author == get_current_user_id()` |
| Inline CSS | Write to static CSS files |
| CDN resources | Bundle locally |
| Missing `permission_callback` on REST | Always specify permission callback |
| `__return_true` on write endpoints | Use proper permission check |
| Unsanitized array input | Use `array_map()` with sanitization |
| Logging personal data | Log IDs + actions only |
| No rate limiting | Add rate limiting for sensitive actions |
| Unlimited query results | Always use `LIMIT` clause |

---

## Checklist for Every Code Change

Before any commit, verify:

- [ ] All prefixes 4+ chars
- [ ] Text domain is `'forum-press-zone'`
- [ ] No inline CSS (`wp_add_inline_style`, `<style>`, `style=""`)
- [ ] No CDN resources
- [ ] All PHP files have ABSPATH check
- [ ] All SQL queries use `$wpdb->prepare()`
- [ ] All output escaped (`esc_html`, `esc_attr`, `esc_url`)
- [ ] All input sanitized (`sanitize_*` functions)
- [ ] All forms/AJAX have nonce verification
- [ ] All sensitive actions have permission checks
- [ ] REST endpoints have `permission_callback`
- [ ] No `__return_true` on write endpoints
- [ ] Ownership validated for user-specific resources
- [ ] Admins protected from moderation
- [ ] No `unserialize()` on untrusted data
- [ ] File uploads use `wp_handle_upload()`
- [ ] No personal data in logs
- [ ] Rate limiting on sensitive actions
- [ ] Query results limited

---

## Integration with Specialized Skills

Foundation skill = security + compliance baseline. Specialized skills build on top:

- **php-skill.md** - PHP-specific patterns (assumes foundation security)
- **javascript-skill.md** - JS patterns (assumes foundation XSS prevention)
- **rest-api-skill.md** - REST patterns (assumes foundation permission checks)
- **sql-skill.md** - SQL patterns (assumes foundation injection prevention)

All foundation rules apply when following specialized skills.