@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.
Files changed (62) hide show
  1. package/LICENSE.txt +223 -0
  2. package/README.md +61 -0
  3. package/bin/sonic.js +304 -0
  4. package/lib/index.js +20 -0
  5. package/lib/installer.js +156 -0
  6. package/lib/license.js +48 -0
  7. package/package.json +46 -0
  8. package/plugin/.claude-plugin/plugin.json +13 -0
  9. package/plugin/README.md +100 -0
  10. package/plugin/agents/sonic.md +80 -0
  11. package/plugin/commands/sonic-build.md +145 -0
  12. package/plugin/commands/sonic-help.md +71 -0
  13. package/plugin/skills/accessibility-qa/SKILL.md +160 -0
  14. package/plugin/skills/accessibility-qa/templates/accessibility-qa-report-template.md +123 -0
  15. package/plugin/skills/accessibility-qa/templates/wcag-compliance-statement.md +70 -0
  16. package/plugin/skills/aka-wireframe-wp/SKILL.md +149 -0
  17. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/README.md +190 -0
  18. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/footer.php +49 -0
  19. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/functions.php +395 -0
  20. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/header.php +58 -0
  21. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/index.php +39 -0
  22. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-answer.php +62 -0
  23. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-authority-hub.php +122 -0
  24. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/page-knowledge.php +58 -0
  25. package/plugin/skills/aka-wireframe-wp/assets/aka-framework-theme/style.css +633 -0
  26. package/plugin/skills/aka-wireframe-wp/references/content-generator.md +371 -0
  27. package/plugin/skills/aka-wireframe-wp/references/internal-linker.md +430 -0
  28. package/plugin/skills/aka-wireframe-wp/references/orchestrator.md +269 -0
  29. package/plugin/skills/aka-wireframe-wp/references/prompts-library.md +880 -0
  30. package/plugin/skills/aka-wireframe-wp/references/seo-optimizer.md +433 -0
  31. package/plugin/skills/aka-wireframe-wp/references/strategy-planner.md +317 -0
  32. package/plugin/skills/aka-wireframe-wp/references/wordpress-deployer.md +545 -0
  33. package/plugin/skills/authority-site-builder/SKILL.md +138 -0
  34. package/plugin/skills/brand-philosophy/SKILL.md +77 -0
  35. package/plugin/skills/freepik-spaces/SKILL.md +122 -0
  36. package/plugin/skills/freepik-spaces/docs/automation-guide.md +233 -0
  37. package/plugin/skills/freepik-spaces/docs/research-notes.md +264 -0
  38. package/plugin/skills/freepik-spaces/plans/naseberry-demo-plan.md +320 -0
  39. package/plugin/skills/freepik-spaces/templates/naseberry-demo.json +302 -0
  40. package/plugin/skills/freepik-spaces/templates/saas-demo.json +212 -0
  41. package/plugin/skills/frontend-design/LICENSE.txt +177 -0
  42. package/plugin/skills/frontend-design/SKILL.md +77 -0
  43. package/plugin/skills/programmatic-seo/SKILL.md +236 -0
  44. package/plugin/skills/programmatic-seo/references/playbooks.md +293 -0
  45. package/plugin/skills/seo-qa/SKILL.md +132 -0
  46. package/plugin/skills/seo-qa/templates/schema-localbusiness.json +49 -0
  47. package/plugin/skills/seo-qa/templates/schema-service.json +36 -0
  48. package/plugin/skills/seo-qa/templates/seo-qa-report-template.md +90 -0
  49. package/plugin/skills/visual-identity/SKILL.md +109 -0
  50. package/plugin/skills/visual-identity/templates/style-guide-template.md +108 -0
  51. package/plugin/skills/website-image-gen/SKILL.md +82 -0
  52. package/plugin/skills/website-image-gen/templates/blog-featured.md +56 -0
  53. package/plugin/skills/website-image-gen/templates/hero-service-photo.md +56 -0
  54. package/plugin/skills/wordpress-pro/SKILL.md +105 -0
  55. package/plugin/skills/wordpress-pro/references/gutenberg-blocks.md +870 -0
  56. package/plugin/skills/wordpress-pro/references/hooks-filters.md +845 -0
  57. package/plugin/skills/wordpress-pro/references/performance-security.md +1012 -0
  58. package/plugin/skills/wordpress-pro/references/plugin-architecture.md +1041 -0
  59. package/plugin/skills/wordpress-pro/references/theme-development.md +858 -0
  60. package/plugin/sops/SOP-Sonic 777/authority-site-sop.html +1100 -0
  61. package/plugin/sops/SOP-WORDPRESS-330-PAGE-SITES.md +926 -0
  62. 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