@lebtiga/sonic-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +223 -0
- package/README.md +61 -0
- package/bin/sonic.js +304 -0
- package/lib/index.js +20 -0
- package/lib/installer.js +156 -0
- package/lib/license.js +48 -0
- package/package.json +46 -0
- package/plugin/.claude-plugin/plugin.json +13 -0
- package/plugin/README.md +100 -0
- package/plugin/agents/sonic.md +80 -0
- package/plugin/commands/sonic-build.md +145 -0
- package/plugin/commands/sonic-help.md +71 -0
- package/plugin/skills/accessibility-qa/SKILL.md +160 -0
- package/plugin/skills/accessibility-qa/templates/accessibility-qa-report-template.md +123 -0
- package/plugin/skills/accessibility-qa/templates/wcag-compliance-statement.md +70 -0
- package/plugin/skills/aka-wireframe-wp/SKILL.md +149 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/README.md +190 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/footer.php +49 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/functions.php +395 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/header.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/index.php +39 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-answer.php +62 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-authority-hub.php +122 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-knowledge.php +58 -0
- package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/style.css +633 -0
- package/plugin/skills/aka-wireframe-wp/references/content-generator.md +371 -0
- package/plugin/skills/aka-wireframe-wp/references/internal-linker.md +430 -0
- package/plugin/skills/aka-wireframe-wp/references/orchestrator.md +269 -0
- package/plugin/skills/aka-wireframe-wp/references/prompts-library.md +880 -0
- package/plugin/skills/aka-wireframe-wp/references/seo-optimizer.md +433 -0
- package/plugin/skills/aka-wireframe-wp/references/strategy-planner.md +317 -0
- package/plugin/skills/aka-wireframe-wp/references/wordpress-deployer.md +545 -0
- package/plugin/skills/authority-site-builder/SKILL.md +138 -0
- package/plugin/skills/brand-philosophy/SKILL.md +77 -0
- package/plugin/skills/freepik-spaces/SKILL.md +122 -0
- package/plugin/skills/freepik-spaces/docs/automation-guide.md +233 -0
- package/plugin/skills/freepik-spaces/docs/research-notes.md +264 -0
- package/plugin/skills/freepik-spaces/plans/naseberry-demo-plan.md +320 -0
- package/plugin/skills/freepik-spaces/templates/naseberry-demo.json +302 -0
- package/plugin/skills/freepik-spaces/templates/saas-demo.json +212 -0
- package/plugin/skills/frontend-design/LICENSE.txt +177 -0
- package/plugin/skills/frontend-design/SKILL.md +77 -0
- package/plugin/skills/programmatic-seo/SKILL.md +236 -0
- package/plugin/skills/programmatic-seo/references/playbooks.md +293 -0
- package/plugin/skills/seo-qa/SKILL.md +132 -0
- package/plugin/skills/seo-qa/templates/schema-localbusiness.json +49 -0
- package/plugin/skills/seo-qa/templates/schema-service.json +36 -0
- package/plugin/skills/seo-qa/templates/seo-qa-report-template.md +90 -0
- package/plugin/skills/visual-identity/SKILL.md +109 -0
- package/plugin/skills/visual-identity/templates/style-guide-template.md +108 -0
- package/plugin/skills/website-image-gen/SKILL.md +82 -0
- package/plugin/skills/website-image-gen/templates/blog-featured.md +56 -0
- package/plugin/skills/website-image-gen/templates/hero-service-photo.md +56 -0
- package/plugin/skills/wordpress-pro/SKILL.md +105 -0
- package/plugin/skills/wordpress-pro/references/gutenberg-blocks.md +870 -0
- package/plugin/skills/wordpress-pro/references/hooks-filters.md +845 -0
- package/plugin/skills/wordpress-pro/references/performance-security.md +1012 -0
- package/plugin/skills/wordpress-pro/references/plugin-architecture.md +1041 -0
- package/plugin/skills/wordpress-pro/references/theme-development.md +858 -0
- package/plugin/sops/SOP-Sonic 777/authority-site-sop.html +1100 -0
- package/plugin/sops/SOP-WORDPRESS-330-PAGE-SITES.md +926 -0
- package/scripts/postinstall.js +109 -0
|
@@ -0,0 +1,1012 @@
|
|
|
1
|
+
# Performance & Security
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Performance Optimization
|
|
6
|
+
|
|
7
|
+
### Database Query Optimization
|
|
8
|
+
|
|
9
|
+
```php
|
|
10
|
+
<?php
|
|
11
|
+
declare(strict_types=1);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Efficient database queries
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// BAD: Query inside loop
|
|
18
|
+
foreach ($post_ids as $post_id) {
|
|
19
|
+
$meta = get_post_meta($post_id, 'my_key', true); // N+1 queries!
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// GOOD: Batch query with caching
|
|
23
|
+
function get_posts_with_meta(array $post_ids): array {
|
|
24
|
+
global $wpdb;
|
|
25
|
+
|
|
26
|
+
$ids = implode(',', array_map('intval', $post_ids));
|
|
27
|
+
|
|
28
|
+
// Single query for all meta
|
|
29
|
+
$results = $wpdb->get_results($wpdb->prepare("
|
|
30
|
+
SELECT post_id, meta_value
|
|
31
|
+
FROM {$wpdb->postmeta}
|
|
32
|
+
WHERE post_id IN ({$ids})
|
|
33
|
+
AND meta_key = %s
|
|
34
|
+
", 'my_key'));
|
|
35
|
+
|
|
36
|
+
$meta_map = [];
|
|
37
|
+
foreach ($results as $row) {
|
|
38
|
+
$meta_map[$row->post_id] = $row->meta_value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return $meta_map;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Use proper $wpdb methods with prepared statements
|
|
46
|
+
*/
|
|
47
|
+
function get_custom_data(int $user_id, string $status): array {
|
|
48
|
+
global $wpdb;
|
|
49
|
+
|
|
50
|
+
$table = $wpdb->prefix . 'custom_table';
|
|
51
|
+
|
|
52
|
+
// GOOD: Prepared statement prevents SQL injection
|
|
53
|
+
return $wpdb->get_results($wpdb->prepare("
|
|
54
|
+
SELECT id, title, created_at
|
|
55
|
+
FROM {$table}
|
|
56
|
+
WHERE user_id = %d
|
|
57
|
+
AND status = %s
|
|
58
|
+
ORDER BY created_at DESC
|
|
59
|
+
LIMIT 100
|
|
60
|
+
", $user_id, $status));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Optimize WP_Query
|
|
65
|
+
*/
|
|
66
|
+
$optimized_query = new WP_Query([
|
|
67
|
+
'post_type' => 'product',
|
|
68
|
+
'posts_per_page' => 10,
|
|
69
|
+
'no_found_rows' => true, // Skip SQL_CALC_FOUND_ROWS for pagination
|
|
70
|
+
'update_post_meta_cache' => false, // Skip meta cache if not needed
|
|
71
|
+
'update_post_term_cache' => false, // Skip term cache if not needed
|
|
72
|
+
'fields' => 'ids', // Only get IDs if that's all you need
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Use transients for expensive queries
|
|
77
|
+
*/
|
|
78
|
+
function get_popular_posts(): array {
|
|
79
|
+
$cache_key = 'popular_posts_week';
|
|
80
|
+
$posts = get_transient($cache_key);
|
|
81
|
+
|
|
82
|
+
if ($posts === false) {
|
|
83
|
+
$posts = get_posts([
|
|
84
|
+
'post_type' => 'post',
|
|
85
|
+
'posts_per_page' => 10,
|
|
86
|
+
'meta_key' => 'views_count',
|
|
87
|
+
'orderby' => 'meta_value_num',
|
|
88
|
+
'order' => 'DESC',
|
|
89
|
+
'date_query' => [
|
|
90
|
+
['after' => '1 week ago'],
|
|
91
|
+
],
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
set_transient($cache_key, $posts, HOUR_IN_SECONDS);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return $posts;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Invalidate cache when data changes
|
|
102
|
+
*/
|
|
103
|
+
add_action('save_post', function(int $post_id): void {
|
|
104
|
+
delete_transient('popular_posts_week');
|
|
105
|
+
delete_transient('featured_posts');
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Object Caching
|
|
110
|
+
|
|
111
|
+
```php
|
|
112
|
+
<?php
|
|
113
|
+
/**
|
|
114
|
+
* WordPress object cache (works with Redis, Memcached)
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
// Set cache
|
|
118
|
+
wp_cache_set('my_data', $data, 'my_plugin', 3600);
|
|
119
|
+
|
|
120
|
+
// Get cache
|
|
121
|
+
$data = wp_cache_get('my_data', 'my_plugin');
|
|
122
|
+
if ($data === false) {
|
|
123
|
+
// Cache miss - fetch and set
|
|
124
|
+
$data = expensive_operation();
|
|
125
|
+
wp_cache_set('my_data', $data, 'my_plugin', 3600);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Delete cache
|
|
129
|
+
wp_cache_delete('my_data', 'my_plugin');
|
|
130
|
+
|
|
131
|
+
// Cache with automatic handling
|
|
132
|
+
function get_expensive_data(int $id): mixed {
|
|
133
|
+
$cache_key = 'expensive_data_' . $id;
|
|
134
|
+
$cache_group = 'my_plugin';
|
|
135
|
+
|
|
136
|
+
$data = wp_cache_get($cache_key, $cache_group);
|
|
137
|
+
|
|
138
|
+
if ($data === false) {
|
|
139
|
+
$data = perform_expensive_operation($id);
|
|
140
|
+
wp_cache_set($cache_key, $data, $cache_group, HOUR_IN_SECONDS);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return $data;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fragment caching for expensive HTML
|
|
148
|
+
*/
|
|
149
|
+
function render_sidebar_widget(): void {
|
|
150
|
+
$cache_key = 'sidebar_widget_html';
|
|
151
|
+
$html = get_transient($cache_key);
|
|
152
|
+
|
|
153
|
+
if ($html === false) {
|
|
154
|
+
ob_start();
|
|
155
|
+
// Expensive rendering
|
|
156
|
+
include plugin_dir_path(__FILE__) . 'templates/widget.php';
|
|
157
|
+
$html = ob_get_clean();
|
|
158
|
+
|
|
159
|
+
set_transient($cache_key, $html, 15 * MINUTE_IN_SECONDS);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Asset Optimization
|
|
167
|
+
|
|
168
|
+
```php
|
|
169
|
+
<?php
|
|
170
|
+
/**
|
|
171
|
+
* Efficient script and style loading
|
|
172
|
+
*/
|
|
173
|
+
|
|
174
|
+
// Conditional loading
|
|
175
|
+
add_action('wp_enqueue_scripts', function(): void {
|
|
176
|
+
// Only load on specific pages
|
|
177
|
+
if (!is_page('contact')) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
wp_enqueue_script('contact-form', ...);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Defer non-critical scripts
|
|
185
|
+
add_filter('script_loader_tag', function(string $tag, string $handle): string {
|
|
186
|
+
$defer_scripts = ['analytics', 'social-share', 'comments'];
|
|
187
|
+
|
|
188
|
+
if (in_array($handle, $defer_scripts, true)) {
|
|
189
|
+
return str_replace(' src', ' defer src', $tag);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return $tag;
|
|
193
|
+
}, 10, 2);
|
|
194
|
+
|
|
195
|
+
// Async scripts
|
|
196
|
+
add_filter('script_loader_tag', function(string $tag, string $handle): string {
|
|
197
|
+
if ($handle === 'my-async-script') {
|
|
198
|
+
return str_replace(' src', ' async src', $tag);
|
|
199
|
+
}
|
|
200
|
+
return $tag;
|
|
201
|
+
}, 10, 2);
|
|
202
|
+
|
|
203
|
+
// Preload critical assets
|
|
204
|
+
add_action('wp_head', function(): void {
|
|
205
|
+
$font_url = get_template_directory_uri() . '/assets/fonts/inter.woff2';
|
|
206
|
+
echo '<link rel="preload" href="' . esc_url($font_url) . '" as="font" type="font/woff2" crossorigin>';
|
|
207
|
+
}, 1);
|
|
208
|
+
|
|
209
|
+
// Remove unused scripts/styles
|
|
210
|
+
add_action('wp_enqueue_scripts', function(): void {
|
|
211
|
+
// Remove block library CSS if not using blocks
|
|
212
|
+
if (!is_singular()) {
|
|
213
|
+
wp_dequeue_style('wp-block-library');
|
|
214
|
+
wp_dequeue_style('wp-block-library-theme');
|
|
215
|
+
wp_dequeue_style('global-styles');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Remove emoji scripts
|
|
219
|
+
remove_action('wp_head', 'print_emoji_detection_script', 7);
|
|
220
|
+
remove_action('wp_print_styles', 'print_emoji_styles');
|
|
221
|
+
}, 100);
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Combine/minify inline styles
|
|
225
|
+
*/
|
|
226
|
+
add_action('wp_footer', function(): void {
|
|
227
|
+
// Add critical CSS inline
|
|
228
|
+
$critical_css = file_get_contents(get_template_directory() . '/assets/css/critical.css');
|
|
229
|
+
if ($critical_css) {
|
|
230
|
+
echo '<style id="critical-css">' . $critical_css . '</style>'; // phpcs:ignore
|
|
231
|
+
}
|
|
232
|
+
}, 1);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Image Optimization
|
|
236
|
+
|
|
237
|
+
```php
|
|
238
|
+
<?php
|
|
239
|
+
/**
|
|
240
|
+
* Image optimization techniques
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
// Add custom image sizes
|
|
244
|
+
add_action('after_setup_theme', function(): void {
|
|
245
|
+
add_image_size('card-thumbnail', 400, 300, true);
|
|
246
|
+
add_image_size('hero-image', 1600, 900, true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Lazy load images
|
|
250
|
+
add_filter('wp_get_attachment_image_attributes', function(array $attr): array {
|
|
251
|
+
$attr['loading'] = 'lazy';
|
|
252
|
+
$attr['decoding'] = 'async';
|
|
253
|
+
return $attr;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Add srcset for responsive images
|
|
257
|
+
add_filter('wp_calculate_image_srcset_meta', function(array $image_meta): array {
|
|
258
|
+
// Ensure srcset is calculated
|
|
259
|
+
return $image_meta;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// WebP support (requires server-side conversion)
|
|
263
|
+
add_filter('wp_generate_attachment_metadata', function(array $metadata, int $attachment_id): array {
|
|
264
|
+
$file = get_attached_file($attachment_id);
|
|
265
|
+
$mime = mime_content_type($file);
|
|
266
|
+
|
|
267
|
+
if (in_array($mime, ['image/jpeg', 'image/png'], true)) {
|
|
268
|
+
// Convert to WebP (requires Imagick or GD)
|
|
269
|
+
my_plugin_create_webp_version($file);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return $metadata;
|
|
273
|
+
}, 10, 2);
|
|
274
|
+
|
|
275
|
+
// Serve WebP with fallback
|
|
276
|
+
function get_webp_image_url(string $url): string {
|
|
277
|
+
$webp_url = preg_replace('/\.(jpe?g|png)$/i', '.webp', $url);
|
|
278
|
+
$webp_path = str_replace(
|
|
279
|
+
wp_upload_dir()['baseurl'],
|
|
280
|
+
wp_upload_dir()['basedir'],
|
|
281
|
+
$webp_url
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (file_exists($webp_path)) {
|
|
285
|
+
return $webp_url;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return $url;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Database Cleanup
|
|
293
|
+
|
|
294
|
+
```php
|
|
295
|
+
<?php
|
|
296
|
+
/**
|
|
297
|
+
* Database maintenance
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
// Clean up revisions (run via WP-CLI or cron)
|
|
301
|
+
function cleanup_post_revisions(int $keep = 5): int {
|
|
302
|
+
global $wpdb;
|
|
303
|
+
|
|
304
|
+
$deleted = 0;
|
|
305
|
+
|
|
306
|
+
$posts = $wpdb->get_col("
|
|
307
|
+
SELECT ID FROM {$wpdb->posts}
|
|
308
|
+
WHERE post_type = 'revision'
|
|
309
|
+
AND post_parent IN (
|
|
310
|
+
SELECT ID FROM {$wpdb->posts} WHERE post_type IN ('post', 'page')
|
|
311
|
+
)
|
|
312
|
+
");
|
|
313
|
+
|
|
314
|
+
// Group by parent
|
|
315
|
+
$by_parent = [];
|
|
316
|
+
foreach ($posts as $revision_id) {
|
|
317
|
+
$parent = wp_get_post_parent_id($revision_id);
|
|
318
|
+
$by_parent[$parent][] = $revision_id;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
foreach ($by_parent as $parent_id => $revisions) {
|
|
322
|
+
// Keep most recent $keep revisions
|
|
323
|
+
$to_delete = array_slice($revisions, $keep);
|
|
324
|
+
foreach ($to_delete as $revision_id) {
|
|
325
|
+
wp_delete_post_revision($revision_id);
|
|
326
|
+
$deleted++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return $deleted;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Clean up orphaned meta
|
|
334
|
+
function cleanup_orphaned_postmeta(): int {
|
|
335
|
+
global $wpdb;
|
|
336
|
+
|
|
337
|
+
return $wpdb->query("
|
|
338
|
+
DELETE pm FROM {$wpdb->postmeta} pm
|
|
339
|
+
LEFT JOIN {$wpdb->posts} p ON pm.post_id = p.ID
|
|
340
|
+
WHERE p.ID IS NULL
|
|
341
|
+
");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Clean up transients
|
|
345
|
+
function cleanup_expired_transients(): int {
|
|
346
|
+
global $wpdb;
|
|
347
|
+
|
|
348
|
+
$time = time();
|
|
349
|
+
|
|
350
|
+
return $wpdb->query($wpdb->prepare("
|
|
351
|
+
DELETE a, b FROM {$wpdb->options} a
|
|
352
|
+
INNER JOIN {$wpdb->options} b ON b.option_name = CONCAT('_transient_timeout_', SUBSTRING(a.option_name, 12))
|
|
353
|
+
WHERE a.option_name LIKE %s
|
|
354
|
+
AND b.option_value < %d
|
|
355
|
+
", '_transient_%', $time));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Schedule cleanup
|
|
359
|
+
add_action('init', function(): void {
|
|
360
|
+
if (!wp_next_scheduled('my_plugin_db_cleanup')) {
|
|
361
|
+
wp_schedule_event(time(), 'weekly', 'my_plugin_db_cleanup');
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
add_action('my_plugin_db_cleanup', function(): void {
|
|
366
|
+
cleanup_post_revisions(3);
|
|
367
|
+
cleanup_orphaned_postmeta();
|
|
368
|
+
cleanup_expired_transients();
|
|
369
|
+
});
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
## Security Hardening
|
|
375
|
+
|
|
376
|
+
### Input Sanitization
|
|
377
|
+
|
|
378
|
+
```php
|
|
379
|
+
<?php
|
|
380
|
+
declare(strict_types=1);
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Always sanitize user input
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
// Text fields
|
|
387
|
+
$title = sanitize_text_field($_POST['title'] ?? '');
|
|
388
|
+
$email = sanitize_email($_POST['email'] ?? '');
|
|
389
|
+
$url = esc_url_raw($_POST['url'] ?? '');
|
|
390
|
+
|
|
391
|
+
// Textarea (allows line breaks)
|
|
392
|
+
$content = sanitize_textarea_field($_POST['content'] ?? '');
|
|
393
|
+
|
|
394
|
+
// HTML content (with allowed tags)
|
|
395
|
+
$html = wp_kses_post($_POST['html_content'] ?? '');
|
|
396
|
+
|
|
397
|
+
// Custom allowed HTML
|
|
398
|
+
$allowed_html = [
|
|
399
|
+
'a' => ['href' => [], 'title' => [], 'target' => []],
|
|
400
|
+
'strong' => [],
|
|
401
|
+
'em' => [],
|
|
402
|
+
'p' => ['class' => []],
|
|
403
|
+
];
|
|
404
|
+
$safe_html = wp_kses($_POST['custom_html'] ?? '', $allowed_html);
|
|
405
|
+
|
|
406
|
+
// File names
|
|
407
|
+
$filename = sanitize_file_name($_POST['filename'] ?? '');
|
|
408
|
+
|
|
409
|
+
// Keys (alphanumeric, dashes, underscores)
|
|
410
|
+
$key = sanitize_key($_POST['key'] ?? '');
|
|
411
|
+
|
|
412
|
+
// Arrays
|
|
413
|
+
$ids = array_map('absint', (array) ($_POST['ids'] ?? []));
|
|
414
|
+
$tags = array_map('sanitize_text_field', (array) ($_POST['tags'] ?? []));
|
|
415
|
+
|
|
416
|
+
// Numbers
|
|
417
|
+
$id = absint($_POST['id'] ?? 0);
|
|
418
|
+
$price = (float) filter_var($_POST['price'] ?? 0, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Database-safe queries
|
|
422
|
+
*/
|
|
423
|
+
global $wpdb;
|
|
424
|
+
|
|
425
|
+
// ALWAYS use prepared statements
|
|
426
|
+
$results = $wpdb->get_results($wpdb->prepare("
|
|
427
|
+
SELECT * FROM {$wpdb->prefix}custom_table
|
|
428
|
+
WHERE user_id = %d
|
|
429
|
+
AND status = %s
|
|
430
|
+
AND created_at > %s
|
|
431
|
+
", $user_id, $status, $date));
|
|
432
|
+
|
|
433
|
+
// Insert with proper escaping
|
|
434
|
+
$wpdb->insert(
|
|
435
|
+
$wpdb->prefix . 'custom_table',
|
|
436
|
+
[
|
|
437
|
+
'user_id' => $user_id,
|
|
438
|
+
'title' => $title,
|
|
439
|
+
'content' => $content,
|
|
440
|
+
],
|
|
441
|
+
['%d', '%s', '%s']
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Update with proper escaping
|
|
445
|
+
$wpdb->update(
|
|
446
|
+
$wpdb->prefix . 'custom_table',
|
|
447
|
+
['title' => $new_title],
|
|
448
|
+
['id' => $id],
|
|
449
|
+
['%s'],
|
|
450
|
+
['%d']
|
|
451
|
+
);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Output Escaping
|
|
455
|
+
|
|
456
|
+
```php
|
|
457
|
+
<?php
|
|
458
|
+
/**
|
|
459
|
+
* Always escape output
|
|
460
|
+
*/
|
|
461
|
+
|
|
462
|
+
// HTML context
|
|
463
|
+
echo '<h1>' . esc_html($title) . '</h1>';
|
|
464
|
+
echo '<p>' . esc_html__('Welcome', 'my-plugin') . '</p>';
|
|
465
|
+
|
|
466
|
+
// Attributes
|
|
467
|
+
echo '<input type="text" value="' . esc_attr($value) . '" />';
|
|
468
|
+
echo '<div class="' . esc_attr($class) . '">';
|
|
469
|
+
echo '<div data-config="' . esc_attr(wp_json_encode($config)) . '">';
|
|
470
|
+
|
|
471
|
+
// URLs
|
|
472
|
+
echo '<a href="' . esc_url($url) . '">' . esc_html($text) . '</a>';
|
|
473
|
+
echo '<img src="' . esc_url($image_url) . '" alt="' . esc_attr($alt) . '" />';
|
|
474
|
+
|
|
475
|
+
// JavaScript
|
|
476
|
+
echo '<script>var config = ' . wp_json_encode($config) . ';</script>';
|
|
477
|
+
|
|
478
|
+
// Textarea content
|
|
479
|
+
echo '<textarea>' . esc_textarea($content) . '</textarea>';
|
|
480
|
+
|
|
481
|
+
// Allow specific HTML
|
|
482
|
+
echo wp_kses_post($html_content);
|
|
483
|
+
|
|
484
|
+
// Translation with escaping
|
|
485
|
+
printf(
|
|
486
|
+
/* translators: %s: user name */
|
|
487
|
+
esc_html__('Hello, %s!', 'my-plugin'),
|
|
488
|
+
esc_html($user_name)
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* When outputting large blocks of HTML
|
|
493
|
+
*/
|
|
494
|
+
?>
|
|
495
|
+
<div class="card">
|
|
496
|
+
<h2><?php echo esc_html($card_title); ?></h2>
|
|
497
|
+
<p><?php echo wp_kses_post($card_content); ?></p>
|
|
498
|
+
<a href="<?php echo esc_url($card_link); ?>" class="<?php echo esc_attr($card_class); ?>">
|
|
499
|
+
<?php echo esc_html($card_cta); ?>
|
|
500
|
+
</a>
|
|
501
|
+
</div>
|
|
502
|
+
<?php
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Nonce Verification
|
|
506
|
+
|
|
507
|
+
```php
|
|
508
|
+
<?php
|
|
509
|
+
/**
|
|
510
|
+
* Nonces prevent CSRF attacks
|
|
511
|
+
*/
|
|
512
|
+
|
|
513
|
+
// In form
|
|
514
|
+
function render_settings_form(): void {
|
|
515
|
+
?>
|
|
516
|
+
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>">
|
|
517
|
+
<?php wp_nonce_field('my_plugin_save_settings', 'my_plugin_nonce'); ?>
|
|
518
|
+
<input type="hidden" name="action" value="my_plugin_save_settings" />
|
|
519
|
+
|
|
520
|
+
<!-- Form fields -->
|
|
521
|
+
|
|
522
|
+
<?php submit_button(__('Save Settings', 'my-plugin')); ?>
|
|
523
|
+
</form>
|
|
524
|
+
<?php
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Verify nonce on submission
|
|
528
|
+
add_action('admin_post_my_plugin_save_settings', function(): void {
|
|
529
|
+
// Verify nonce
|
|
530
|
+
if (!wp_verify_nonce($_POST['my_plugin_nonce'] ?? '', 'my_plugin_save_settings')) {
|
|
531
|
+
wp_die(
|
|
532
|
+
esc_html__('Security check failed.', 'my-plugin'),
|
|
533
|
+
esc_html__('Error', 'my-plugin'),
|
|
534
|
+
['response' => 403]
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Verify capability
|
|
539
|
+
if (!current_user_can('manage_options')) {
|
|
540
|
+
wp_die(
|
|
541
|
+
esc_html__('You do not have permission to perform this action.', 'my-plugin'),
|
|
542
|
+
esc_html__('Error', 'my-plugin'),
|
|
543
|
+
['response' => 403]
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Process form
|
|
548
|
+
$settings = [
|
|
549
|
+
'option_1' => sanitize_text_field($_POST['option_1'] ?? ''),
|
|
550
|
+
'option_2' => absint($_POST['option_2'] ?? 0),
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
update_option('my_plugin_settings', $settings);
|
|
554
|
+
|
|
555
|
+
wp_safe_redirect(add_query_arg('updated', 'true', wp_get_referer()));
|
|
556
|
+
exit;
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* AJAX with nonce
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
// Localize script with nonce
|
|
564
|
+
wp_localize_script('my-script', 'myPluginData', [
|
|
565
|
+
'ajaxUrl' => admin_url('admin-ajax.php'),
|
|
566
|
+
'nonce' => wp_create_nonce('my_plugin_ajax'),
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
// JavaScript
|
|
570
|
+
// fetch(myPluginData.ajaxUrl, {
|
|
571
|
+
// method: 'POST',
|
|
572
|
+
// headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
573
|
+
// body: new URLSearchParams({
|
|
574
|
+
// action: 'my_plugin_action',
|
|
575
|
+
// nonce: myPluginData.nonce,
|
|
576
|
+
// data: 'value'
|
|
577
|
+
// })
|
|
578
|
+
// });
|
|
579
|
+
|
|
580
|
+
// Handle AJAX
|
|
581
|
+
add_action('wp_ajax_my_plugin_action', function(): void {
|
|
582
|
+
check_ajax_referer('my_plugin_ajax', 'nonce');
|
|
583
|
+
|
|
584
|
+
if (!current_user_can('edit_posts')) {
|
|
585
|
+
wp_send_json_error(['message' => 'Unauthorized'], 403);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
$data = sanitize_text_field($_POST['data'] ?? '');
|
|
589
|
+
|
|
590
|
+
// Process request
|
|
591
|
+
$result = process_data($data);
|
|
592
|
+
|
|
593
|
+
wp_send_json_success(['result' => $result]);
|
|
594
|
+
});
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Capability Checks
|
|
598
|
+
|
|
599
|
+
```php
|
|
600
|
+
<?php
|
|
601
|
+
/**
|
|
602
|
+
* Always verify user capabilities
|
|
603
|
+
*/
|
|
604
|
+
|
|
605
|
+
// Check before displaying admin page
|
|
606
|
+
function render_admin_page(): void {
|
|
607
|
+
if (!current_user_can('manage_options')) {
|
|
608
|
+
wp_die(__('You do not have permission to access this page.', 'my-plugin'));
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Render page
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Check before performing action
|
|
615
|
+
function delete_item(int $item_id): bool {
|
|
616
|
+
if (!current_user_can('delete_posts')) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Delete item
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Meta capability check for specific post
|
|
625
|
+
function edit_custom_post(int $post_id): bool {
|
|
626
|
+
if (!current_user_can('edit_post', $post_id)) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Edit post
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Custom capabilities
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
// Add custom capabilities on activation
|
|
639
|
+
function add_custom_capabilities(): void {
|
|
640
|
+
$admin = get_role('administrator');
|
|
641
|
+
$editor = get_role('editor');
|
|
642
|
+
|
|
643
|
+
if ($admin) {
|
|
644
|
+
$admin->add_cap('manage_my_plugin');
|
|
645
|
+
$admin->add_cap('edit_my_plugin_items');
|
|
646
|
+
$admin->add_cap('delete_my_plugin_items');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if ($editor) {
|
|
650
|
+
$editor->add_cap('edit_my_plugin_items');
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Use custom capability
|
|
655
|
+
if (current_user_can('manage_my_plugin')) {
|
|
656
|
+
// Show management interface
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Map meta capabilities
|
|
660
|
+
add_filter('map_meta_cap', function(array $caps, string $cap, int $user_id, array $args): array {
|
|
661
|
+
if ($cap === 'edit_my_plugin_item') {
|
|
662
|
+
$item_id = $args[0] ?? 0;
|
|
663
|
+
$item = get_my_plugin_item($item_id);
|
|
664
|
+
|
|
665
|
+
if ($item && $item->author_id === $user_id) {
|
|
666
|
+
$caps = ['edit_my_plugin_items'];
|
|
667
|
+
} else {
|
|
668
|
+
$caps = ['manage_my_plugin'];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return $caps;
|
|
673
|
+
}, 10, 4);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### File Upload Security
|
|
677
|
+
|
|
678
|
+
```php
|
|
679
|
+
<?php
|
|
680
|
+
/**
|
|
681
|
+
* Secure file upload handling
|
|
682
|
+
*/
|
|
683
|
+
|
|
684
|
+
function handle_file_upload(): array|WP_Error {
|
|
685
|
+
// Verify nonce and capability
|
|
686
|
+
if (!wp_verify_nonce($_POST['nonce'] ?? '', 'my_plugin_upload')) {
|
|
687
|
+
return new WP_Error('security', __('Security check failed.', 'my-plugin'));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (!current_user_can('upload_files')) {
|
|
691
|
+
return new WP_Error('permission', __('You cannot upload files.', 'my-plugin'));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// Check file exists
|
|
695
|
+
if (empty($_FILES['my_file']['tmp_name'])) {
|
|
696
|
+
return new WP_Error('no_file', __('No file uploaded.', 'my-plugin'));
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
$file = $_FILES['my_file'];
|
|
700
|
+
|
|
701
|
+
// Validate file type
|
|
702
|
+
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
|
|
703
|
+
$file_type = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
|
|
704
|
+
|
|
705
|
+
if (!in_array($file_type['type'], $allowed_types, true)) {
|
|
706
|
+
return new WP_Error('invalid_type', __('File type not allowed.', 'my-plugin'));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Validate file size (5MB max)
|
|
710
|
+
$max_size = 5 * 1024 * 1024;
|
|
711
|
+
if ($file['size'] > $max_size) {
|
|
712
|
+
return new WP_Error('too_large', __('File is too large.', 'my-plugin'));
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Sanitize filename
|
|
716
|
+
$filename = sanitize_file_name($file['name']);
|
|
717
|
+
|
|
718
|
+
// Use WordPress upload handling
|
|
719
|
+
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
720
|
+
require_once ABSPATH . 'wp-admin/includes/media.php';
|
|
721
|
+
require_once ABSPATH . 'wp-admin/includes/image.php';
|
|
722
|
+
|
|
723
|
+
// Handle upload
|
|
724
|
+
$upload = wp_handle_upload($file, [
|
|
725
|
+
'test_form' => false,
|
|
726
|
+
'mimes' => [
|
|
727
|
+
'jpg|jpeg' => 'image/jpeg',
|
|
728
|
+
'png' => 'image/png',
|
|
729
|
+
'gif' => 'image/gif',
|
|
730
|
+
'pdf' => 'application/pdf',
|
|
731
|
+
],
|
|
732
|
+
]);
|
|
733
|
+
|
|
734
|
+
if (isset($upload['error'])) {
|
|
735
|
+
return new WP_Error('upload_error', $upload['error']);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Create attachment
|
|
739
|
+
$attachment_id = wp_insert_attachment([
|
|
740
|
+
'post_mime_type' => $upload['type'],
|
|
741
|
+
'post_title' => preg_replace('/\.[^.]+$/', '', $filename),
|
|
742
|
+
'post_content' => '',
|
|
743
|
+
'post_status' => 'inherit',
|
|
744
|
+
], $upload['file']);
|
|
745
|
+
|
|
746
|
+
// Generate metadata
|
|
747
|
+
$metadata = wp_generate_attachment_metadata($attachment_id, $upload['file']);
|
|
748
|
+
wp_update_attachment_metadata($attachment_id, $metadata);
|
|
749
|
+
|
|
750
|
+
return [
|
|
751
|
+
'id' => $attachment_id,
|
|
752
|
+
'url' => $upload['url'],
|
|
753
|
+
];
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Security Headers
|
|
758
|
+
|
|
759
|
+
```php
|
|
760
|
+
<?php
|
|
761
|
+
/**
|
|
762
|
+
* Add security headers
|
|
763
|
+
*/
|
|
764
|
+
|
|
765
|
+
add_action('send_headers', function(): void {
|
|
766
|
+
// Only on frontend, not admin or REST
|
|
767
|
+
if (is_admin() || defined('REST_REQUEST')) {
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Prevent clickjacking
|
|
772
|
+
header('X-Frame-Options: SAMEORIGIN');
|
|
773
|
+
|
|
774
|
+
// Prevent MIME sniffing
|
|
775
|
+
header('X-Content-Type-Options: nosniff');
|
|
776
|
+
|
|
777
|
+
// XSS Protection
|
|
778
|
+
header('X-XSS-Protection: 1; mode=block');
|
|
779
|
+
|
|
780
|
+
// Referrer Policy
|
|
781
|
+
header('Referrer-Policy: strict-origin-when-cross-origin');
|
|
782
|
+
|
|
783
|
+
// Content Security Policy (customize as needed)
|
|
784
|
+
$csp = "default-src 'self'; " .
|
|
785
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com; " .
|
|
786
|
+
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " .
|
|
787
|
+
"font-src 'self' https://fonts.gstatic.com; " .
|
|
788
|
+
"img-src 'self' data: https:; " .
|
|
789
|
+
"connect-src 'self' https://www.google-analytics.com;";
|
|
790
|
+
|
|
791
|
+
header("Content-Security-Policy: {$csp}");
|
|
792
|
+
|
|
793
|
+
// Permissions Policy
|
|
794
|
+
header("Permissions-Policy: geolocation=(), microphone=(), camera=()");
|
|
795
|
+
});
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Login Security
|
|
799
|
+
|
|
800
|
+
```php
|
|
801
|
+
<?php
|
|
802
|
+
/**
|
|
803
|
+
* Login security enhancements
|
|
804
|
+
*/
|
|
805
|
+
|
|
806
|
+
// Limit login attempts
|
|
807
|
+
add_filter('authenticate', function(?WP_User $user, string $username, string $password): WP_User|WP_Error|null {
|
|
808
|
+
if (empty($username) || empty($password)) {
|
|
809
|
+
return $user;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
$ip = $_SERVER['REMOTE_ADDR'];
|
|
813
|
+
$lockout_key = 'login_attempts_' . md5($ip);
|
|
814
|
+
$attempts = (int) get_transient($lockout_key);
|
|
815
|
+
|
|
816
|
+
if ($attempts >= 5) {
|
|
817
|
+
return new WP_Error(
|
|
818
|
+
'too_many_attempts',
|
|
819
|
+
sprintf(
|
|
820
|
+
__('Too many failed login attempts. Please try again in %d minutes.', 'my-plugin'),
|
|
821
|
+
15
|
|
822
|
+
)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return $user;
|
|
827
|
+
}, 20, 3);
|
|
828
|
+
|
|
829
|
+
// Track failed attempts
|
|
830
|
+
add_action('wp_login_failed', function(string $username): void {
|
|
831
|
+
$ip = $_SERVER['REMOTE_ADDR'];
|
|
832
|
+
$lockout_key = 'login_attempts_' . md5($ip);
|
|
833
|
+
$attempts = (int) get_transient($lockout_key);
|
|
834
|
+
|
|
835
|
+
set_transient($lockout_key, $attempts + 1, 15 * MINUTE_IN_SECONDS);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Clear on successful login
|
|
839
|
+
add_action('wp_login', function(string $username): void {
|
|
840
|
+
$ip = $_SERVER['REMOTE_ADDR'];
|
|
841
|
+
$lockout_key = 'login_attempts_' . md5($ip);
|
|
842
|
+
delete_transient($lockout_key);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// Disable XML-RPC if not needed
|
|
846
|
+
add_filter('xmlrpc_enabled', '__return_false');
|
|
847
|
+
|
|
848
|
+
// Hide login errors (don't reveal if username exists)
|
|
849
|
+
add_filter('login_errors', function(): string {
|
|
850
|
+
return __('Invalid login credentials.', 'my-plugin');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Force strong passwords
|
|
854
|
+
add_action('user_profile_update_errors', function(WP_Error $errors, bool $update, object $user): void {
|
|
855
|
+
$password = $_POST['pass1'] ?? '';
|
|
856
|
+
|
|
857
|
+
if (!empty($password)) {
|
|
858
|
+
// Require minimum 12 characters
|
|
859
|
+
if (strlen($password) < 12) {
|
|
860
|
+
$errors->add('weak_password', __('Password must be at least 12 characters.', 'my-plugin'));
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Require mixed case, numbers, special chars
|
|
864
|
+
if (!preg_match('/[A-Z]/', $password) ||
|
|
865
|
+
!preg_match('/[a-z]/', $password) ||
|
|
866
|
+
!preg_match('/[0-9]/', $password) ||
|
|
867
|
+
!preg_match('/[^A-Za-z0-9]/', $password)) {
|
|
868
|
+
$errors->add('weak_password', __('Password must contain uppercase, lowercase, numbers, and special characters.', 'my-plugin'));
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}, 10, 3);
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## Backup Strategy
|
|
877
|
+
|
|
878
|
+
```php
|
|
879
|
+
<?php
|
|
880
|
+
/**
|
|
881
|
+
* Backup implementation
|
|
882
|
+
*/
|
|
883
|
+
|
|
884
|
+
class Database_Backup {
|
|
885
|
+
|
|
886
|
+
private string $backup_dir;
|
|
887
|
+
|
|
888
|
+
public function __construct() {
|
|
889
|
+
$upload_dir = wp_upload_dir();
|
|
890
|
+
$this->backup_dir = $upload_dir['basedir'] . '/backups/';
|
|
891
|
+
|
|
892
|
+
if (!file_exists($this->backup_dir)) {
|
|
893
|
+
wp_mkdir_p($this->backup_dir);
|
|
894
|
+
|
|
895
|
+
// Protect directory
|
|
896
|
+
file_put_contents($this->backup_dir . '.htaccess', 'deny from all');
|
|
897
|
+
file_put_contents($this->backup_dir . 'index.php', '<?php // Silence is golden');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Create database backup
|
|
903
|
+
*/
|
|
904
|
+
public function create_backup(): string|WP_Error {
|
|
905
|
+
global $wpdb;
|
|
906
|
+
|
|
907
|
+
$filename = 'db-backup-' . date('Y-m-d-His') . '.sql';
|
|
908
|
+
$filepath = $this->backup_dir . $filename;
|
|
909
|
+
|
|
910
|
+
$tables = $wpdb->get_col('SHOW TABLES');
|
|
911
|
+
$output = "-- WordPress Database Backup\n";
|
|
912
|
+
$output .= "-- Generated: " . date('Y-m-d H:i:s') . "\n\n";
|
|
913
|
+
|
|
914
|
+
foreach ($tables as $table) {
|
|
915
|
+
// Skip non-WordPress tables
|
|
916
|
+
if (strpos($table, $wpdb->prefix) !== 0) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
$output .= "DROP TABLE IF EXISTS `{$table}`;\n";
|
|
921
|
+
|
|
922
|
+
$create = $wpdb->get_row("SHOW CREATE TABLE `{$table}`", ARRAY_N);
|
|
923
|
+
$output .= $create[1] . ";\n\n";
|
|
924
|
+
|
|
925
|
+
$rows = $wpdb->get_results("SELECT * FROM `{$table}`", ARRAY_A);
|
|
926
|
+
|
|
927
|
+
foreach ($rows as $row) {
|
|
928
|
+
$values = array_map(function($value) use ($wpdb) {
|
|
929
|
+
return $value === null ? 'NULL' : "'" . $wpdb->_real_escape($value) . "'";
|
|
930
|
+
}, $row);
|
|
931
|
+
|
|
932
|
+
$output .= "INSERT INTO `{$table}` VALUES (" . implode(',', $values) . ");\n";
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
$output .= "\n";
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (file_put_contents($filepath, $output) === false) {
|
|
939
|
+
return new WP_Error('backup_failed', __('Failed to write backup file.', 'my-plugin'));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Compress
|
|
943
|
+
if (function_exists('gzencode')) {
|
|
944
|
+
$compressed = gzencode($output, 9);
|
|
945
|
+
file_put_contents($filepath . '.gz', $compressed);
|
|
946
|
+
unlink($filepath);
|
|
947
|
+
$filepath .= '.gz';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return $filepath;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Clean old backups
|
|
955
|
+
*/
|
|
956
|
+
public function cleanup_old_backups(int $keep_days = 30): int {
|
|
957
|
+
$deleted = 0;
|
|
958
|
+
$files = glob($this->backup_dir . '*.sql*');
|
|
959
|
+
$cutoff = time() - ($keep_days * DAY_IN_SECONDS);
|
|
960
|
+
|
|
961
|
+
foreach ($files as $file) {
|
|
962
|
+
if (filemtime($file) < $cutoff) {
|
|
963
|
+
unlink($file);
|
|
964
|
+
$deleted++;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return $deleted;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Schedule automatic backups
|
|
973
|
+
add_action('init', function(): void {
|
|
974
|
+
if (!wp_next_scheduled('my_plugin_daily_backup')) {
|
|
975
|
+
wp_schedule_event(time(), 'daily', 'my_plugin_daily_backup');
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
add_action('my_plugin_daily_backup', function(): void {
|
|
980
|
+
$backup = new Database_Backup();
|
|
981
|
+
$result = $backup->create_backup();
|
|
982
|
+
|
|
983
|
+
if (!is_wp_error($result)) {
|
|
984
|
+
$backup->cleanup_old_backups(7);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
---
|
|
990
|
+
|
|
991
|
+
## Best Practices Summary
|
|
992
|
+
|
|
993
|
+
### Performance
|
|
994
|
+
|
|
995
|
+
- Use object caching (Redis/Memcached)
|
|
996
|
+
- Implement transients for expensive operations
|
|
997
|
+
- Optimize database queries with proper indexes
|
|
998
|
+
- Lazy load images and defer non-critical scripts
|
|
999
|
+
- Use `no_found_rows` when pagination not needed
|
|
1000
|
+
- Clean up revisions and transients regularly
|
|
1001
|
+
|
|
1002
|
+
### Security
|
|
1003
|
+
|
|
1004
|
+
- Sanitize ALL user input
|
|
1005
|
+
- Escape ALL output
|
|
1006
|
+
- Use nonces for all form submissions and AJAX
|
|
1007
|
+
- Verify capabilities before any action
|
|
1008
|
+
- Use prepared statements for database queries
|
|
1009
|
+
- Validate file uploads thoroughly
|
|
1010
|
+
- Implement rate limiting for login
|
|
1011
|
+
- Add security headers
|
|
1012
|
+
- Keep WordPress and plugins updated
|