📖 WordPress Development - Security, Performance và Deployment
70 phút

Security, Performance và Deployment

Security Best Practices

Sanitize inputs

// Text field
$text = sanitize_text_field($_POST['text']);

// Email
$email = sanitize_email($_POST['email']);

// URL
$url = esc_url_raw($_POST['url']);

// Textarea
$content = sanitize_textarea_field($_POST['content']);

// HTML content
$html = wp_kses_post($_POST['html']);

// Custom allowed HTML
$html = wp_kses($_POST['html'], [
    'a' => ['href' => [], 'title' => []],
    'strong' => [],
    'em' => [],
]);

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

Escape outputs

// HTML text
echo esc_html($text);

// HTML attribute
echo esc_attr($attribute);

// URL
echo esc_url($url);

// JavaScript
echo esc_js($js_string);

// Textarea
echo esc_textarea($textarea);

// With translation
echo esc_html__('Text', 'my-plugin');

// printf patterns
printf(
    '<a href="%s" title="%s">%s</a>',
    esc_url($url),
    esc_attr($title),
    esc_html($link_text)
);

Nonces

// Form
<form method="post">
    <?php wp_nonce_field('my_action', 'my_nonce'); ?>
    <input type="text" name="data">
    <button type="submit">Save</button>
</form>

// Verify
if (!isset($_POST['my_nonce']) ||
    !wp_verify_nonce($_POST['my_nonce'], 'my_action')) {
    wp_die('Security check failed');
}

// Ajax
wp_localize_script('my-script', 'myData', [
    'nonce' => wp_create_nonce('my_ajax_nonce'),
]);

// Verify trong ajax handler
check_ajax_referer('my_ajax_nonce', 'nonce');

Capability checks

if (!current_user_can('manage_options')) {
    wp_die('Access denied');
}

if (!current_user_can('edit_post', $post_id)) {
    wp_die('You cannot edit this post');
}

// Trong AJAX
if (!current_user_can('edit_posts')) {
    wp_send_json_error('Unauthorized', 403);
}

SQL Injection

// Không tốt
$wpdb->query("SELECT * FROM table WHERE id = $id");

// Tốt - dùng prepare
$wpdb->prepare("SELECT * FROM table WHERE id = %d", $id);

// Với LIKE
$wpdb->prepare("SELECT * FROM table WHERE name LIKE %s",
    '%' . $wpdb->esc_like($term) . '%');

// Full query
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}books WHERE author = %s AND year > %d",
    $author, $year
));

File uploads

if (!function_exists('wp_handle_upload')) {
    require_once ABSPATH . 'wp-admin/includes/file.php';
}

$allowed_types = ['image/jpeg', 'image/png', 'application/pdf'];

$uploaded = wp_handle_upload($_FILES['file'], [
    'test_form' => false,
    'mimes'     => [
        'jpg|jpeg' => 'image/jpeg',
        'png'      => 'image/png',
        'pdf'      => 'application/pdf',
    ],
]);

if (isset($uploaded['error'])) {
    wp_die($uploaded['error']);
}

// Insert to media library
$attachment = [
    'post_mime_type' => $uploaded['type'],
    'post_title'     => sanitize_file_name(basename($uploaded['file'])),
    'post_content'   => '',
    'post_status'    => 'inherit',
];

$attach_id = wp_insert_attachment($attachment, $uploaded['file']);
require_once ABSPATH . 'wp-admin/includes/image.php';
$metadata = wp_generate_attachment_metadata($attach_id, $uploaded['file']);
wp_update_attachment_metadata($attach_id, $metadata);

Performance Optimization

Caching

// Transients
$data = get_transient('my_plugin_expensive_data');

if (false === $data) {
    $data = expensive_operation();
    set_transient('my_plugin_expensive_data', $data, HOUR_IN_SECONDS);
}

// Cache invalidation
delete_transient('my_plugin_expensive_data');

// Object cache (Redis/Memcached)
wp_cache_set('my_key', $value, 'my_group', 3600);
$value = wp_cache_get('my_key', 'my_group');

// Cache WP_Query
$query = new WP_Query([
    'post_type'      => 'post',
    'posts_per_page' => 10,
    'no_found_rows'  => true,           // Nếu không cần pagination
    'update_post_meta_cache' => false,   // Nếu không cần meta
    'update_post_term_cache' => false,   // Nếu không cần terms
]);

Query optimization

// Không tốt - N+1 queries
foreach ($posts as $post) {
    $author = get_the_author_meta('display_name', $post->post_author);
}

// Tốt - preload
$author_ids = wp_list_pluck($posts, 'post_author');
$authors = get_users(['include' => array_unique($author_ids)]);

// Meta query optimization
$query = new WP_Query([
    'post_type'  => 'book',
    'meta_query' => [
        'relation' => 'AND',
        [
            'key'     => 'price',
            'value'   => 100,
            'compare' => '<=',
            'type'    => 'NUMERIC',
        ],
    ],
    'meta_key'   => 'price',
    'orderby'    => 'meta_value_num',
]);

Enqueue assets properly

// Chỉ load khi cần
function my_plugin_enqueue() {
    // Chỉ load trên page cụ thể
    if (!is_page('contact')) {
        return;
    }

    wp_enqueue_script(
        'my-plugin-contact',
        plugins_url('js/contact.js', __FILE__),
        [],
        '1.0.0',
        true
    );
}
add_action('wp_enqueue_scripts', 'my_plugin_enqueue');

// Async/Defer
add_filter('script_loader_tag', function ($tag, $handle) {
    if ('my-plugin-analytics' !== $handle) {
        return $tag;
    }
    return str_replace(' src', ' async src', $tag);
}, 10, 2);

Database indexes

// Trong activation
$sql = "CREATE TABLE {$wpdb->prefix}books (
    id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    title varchar(255) NOT NULL,
    author varchar(255) NOT NULL,
    year int(4) DEFAULT NULL,
    PRIMARY KEY (id),
    KEY author (author),
    KEY year (year),
    KEY title_author (title(100), author(100))
) $charset_collate;";

Deployment

Version control

# .gitignore
wp-config.php
wp-content/uploads/
wp-content/upgrade/
wp-content/cache/
*.log
.env
node_modules/
vendor/

WP-CLI deploy

# Sync files
rsync -avz --exclude='.git' --exclude='node_modules' \
    ./ user@server:/var/www/html/wp-content/plugins/my-plugin/

# SSH and run commands
ssh user@server
cd /var/www/html
wp plugin activate my-plugin
wp cache flush

CI/CD với GitHub Actions

name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install deps
        run: npm ci

      - name: Build assets
        run: npm run build

      - name: Deploy via SSH
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_KEY }}
          REMOTE_HOST: ${{ secrets.HOST }}
          REMOTE_USER: ${{ secrets.USER }}
          SOURCE: "./"
          TARGET: "/var/www/html/wp-content/plugins/my-plugin/"
          EXCLUDE: "/node_modules/, /.git/, /src/"

Backup strategies

# Backup database
wp db export backup-$(date +%Y%m%d).sql

# Restore
wp db import backup-20240101.sql

# Backup files
tar -czf backup-files-$(date +%Y%m%d).tar.gz wp-content/

# Automated backup script
#!/bin/bash
BACKUP_DIR="/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# Database
wp db export "$BACKUP_DIR/db_$DATE.sql" --path=/var/www/html

# Files
tar -czf "$BACKUP_DIR/files_$DATE.tar.gz" -C /var/www/html wp-content

# Cleanup old backups (keep 30 days)
find "$BACKUP_DIR" -type f -mtime +30 -delete

Security hardening

// wp-config.php
define('DISALLOW_FILE_EDIT', true);
define('DISALLOW_FILE_MODS', true); // Disable plugin/theme installation via admin
define('FORCE_SSL_ADMIN', true);
define('WP_AUTO_UPDATE_CORE', 'minor');

// Disable XML-RPC
add_filter('xmlrpc_enabled', '__return_false');

// Remove WP version
remove_action('wp_head', 'wp_generator');
add_filter('the_generator', '__return_empty_string');

// Disable file editing
add_filter('wp_headers', function ($headers) {
    unset($headers['X-Pingback']);
    return $headers;
});

// Limit login attempts (dùng plugin như Limit Login Attempts)

// Force strong passwords
add_action('user_profile_update_errors', function ($errors, $update, $user) {
    if (!empty($_POST['pass1'])) {
        $strength = 0;
        if (strlen($_POST['pass1']) >= 12) $strength++;
        if (preg_match('/[A-Z]/', $_POST['pass1'])) $strength++;
        if (preg_match('/[0-9]/', $_POST['pass1'])) $strength++;
        if (preg_match('/[^A-Za-z0-9]/', $_POST['pass1'])) $strength++;

        if ($strength < 3) {
            $errors->add('weak_password', 'Password must be stronger.');
        }
    }
}, 10, 3);

.htaccess security

# Protect wp-config.php
<files wp-config.php>
    order allow,deny
    deny from all
</files>

# Protect .htaccess
<files ~ "^.*\.([Hh][Tt][Aa])">
    order allow,deny
    deny from all
    satisfy all
</files>

# Disable directory listing
Options -Indexes

# Protect wp-includes
<IfModule mod_rewrite.c>
    RewriteRule ^wp-admin/includes/ - [F,L]
    RewriteRule !^wp-includes/ - [S=3]
    RewriteRule ^wp-includes/[^/]+\.php$ - [F,L]
    RewriteRule ^wp-includes/js/tinymce/langs/.+\.php - [F,L]
    RewriteRule ^wp-includes/theme-compat/ - [F,L]
</IfModule>

# Security headers
<IfModule mod_headers.c>
    Header set X-Content-Type-Options "nosniff"
    Header set X-Frame-Options "SAMEORIGIN"
    Header set X-XSS-Protection "1; mode=block"
    Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>

# Enable compression
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript
</IfModule>

# Browser caching
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType text/css "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
</IfModule>

Bài tập thực hành

Hãy bảo mật và optimize plugin!

📝 Bài tập (1)

  1. Bảo mật và tối ưu plugin Book Manager