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!