@mytechtoday/augment-extensions 0.1.1 → 0.2.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/augment-extensions/domain-rules/wordpress/README.md +163 -0
- package/augment-extensions/domain-rules/wordpress/module.json +32 -0
- package/augment-extensions/domain-rules/wordpress/rules/coding-standards.md +617 -0
- package/augment-extensions/domain-rules/wordpress/rules/directory-structure.md +270 -0
- package/augment-extensions/domain-rules/wordpress/rules/file-patterns.md +423 -0
- package/augment-extensions/domain-rules/wordpress/rules/gutenberg-blocks.md +493 -0
- package/augment-extensions/domain-rules/wordpress/rules/performance.md +568 -0
- package/augment-extensions/domain-rules/wordpress/rules/plugin-development.md +510 -0
- package/augment-extensions/domain-rules/wordpress/rules/project-detection.md +251 -0
- package/augment-extensions/domain-rules/wordpress/rules/rest-api.md +501 -0
- package/augment-extensions/domain-rules/wordpress/rules/security.md +564 -0
- package/augment-extensions/domain-rules/wordpress/rules/theme-development.md +388 -0
- package/augment-extensions/domain-rules/wordpress/rules/woocommerce.md +441 -0
- package/augment-extensions/domain-rules/wordpress-plugin/README.md +139 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/ajax-plugin.md +1599 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/custom-post-type-plugin.md +1727 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/gutenberg-block-plugin.md +428 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/gutenberg-block.md +422 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/mvc-plugin.md +1623 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/object-oriented-plugin.md +1343 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/rest-endpoint.md +734 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/settings-page-plugin.md +1350 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/simple-procedural-plugin.md +503 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/singleton-plugin.md +971 -0
- package/augment-extensions/domain-rules/wordpress-plugin/module.json +53 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/activation-hooks.md +770 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/admin-interface.md +874 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/ajax-handlers.md +629 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/asset-management.md +559 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/context-providers.md +709 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/cron-jobs.md +736 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/database-management.md +1057 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/documentation-standards.md +463 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/frontend-functionality.md +478 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/gutenberg-blocks.md +818 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/internationalization.md +416 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/migration.md +667 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/performance-optimization.md +878 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/plugin-architecture.md +693 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/plugin-structure.md +352 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/rest-api.md +818 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/scaffolding-workflow.md +624 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/security-best-practices.md +866 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/testing-patterns.md +1165 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/testing.md +414 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/vscode-integration.md +751 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/woocommerce-integration.md +949 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/wordpress-org-submission.md +458 -0
- package/augment-extensions/examples/gutenberg-block-plugin/README.md +101 -0
- package/augment-extensions/examples/gutenberg-block-plugin/examples/testimonial-block.md +428 -0
- package/augment-extensions/examples/gutenberg-block-plugin/module.json +40 -0
- package/augment-extensions/examples/rest-api-plugin/README.md +98 -0
- package/augment-extensions/examples/rest-api-plugin/examples/task-manager-api.md +1299 -0
- package/augment-extensions/examples/rest-api-plugin/module.json +40 -0
- package/augment-extensions/examples/woocommerce-extension/README.md +98 -0
- package/augment-extensions/examples/woocommerce-extension/examples/product-customizer.md +763 -0
- package/augment-extensions/examples/woocommerce-extension/module.json +40 -0
- package/augment-extensions/workflows/wordpress-plugin/README.md +232 -0
- package/augment-extensions/workflows/wordpress-plugin/ai-prompts.md +839 -0
- package/augment-extensions/workflows/wordpress-plugin/bead-decomposition-patterns.md +854 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/complete-plugin-example.md +540 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/custom-post-type-example.md +1083 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/feature-addition-workflow.md +669 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/plugin-creation-workflow.md +597 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/secure-form-handler-example.md +925 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/security-audit-workflow.md +752 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/wordpress-org-submission-workflow.md +773 -0
- package/augment-extensions/workflows/wordpress-plugin/module.json +49 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/best-practices.md +942 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/development-workflow.md +702 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/submission-workflow.md +728 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/testing-workflow.md +775 -0
- package/cli/dist/cli.js +5 -1
- package/cli/dist/cli.js.map +1 -1
- package/cli/dist/commands/show.d.ts.map +1 -1
- package/cli/dist/commands/show.js +41 -0
- package/cli/dist/commands/show.js.map +1 -1
- package/modules.md +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
# Secure Form Handler Example
|
|
2
|
+
|
|
3
|
+
This example demonstrates implementing a secure form handler in WordPress with comprehensive security measures including nonce verification, capability checks, input sanitization, and output escaping.
|
|
4
|
+
|
|
5
|
+
## Scenario
|
|
6
|
+
|
|
7
|
+
Creating a secure contact form handler that demonstrates all WordPress security best practices.
|
|
8
|
+
|
|
9
|
+
## Complete Implementation
|
|
10
|
+
|
|
11
|
+
### Form Display
|
|
12
|
+
|
|
13
|
+
**File**: `public/partials/contact-form.php`
|
|
14
|
+
|
|
15
|
+
```php
|
|
16
|
+
<?php
|
|
17
|
+
/**
|
|
18
|
+
* Contact form template.
|
|
19
|
+
*
|
|
20
|
+
* Security features:
|
|
21
|
+
* - Nonce field for CSRF protection
|
|
22
|
+
* - Escaped output for XSS prevention
|
|
23
|
+
* - Honeypot field for spam prevention
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
// Get current values (for form repopulation after validation errors)
|
|
27
|
+
$name = isset($_POST['contact_name']) ? sanitize_text_field($_POST['contact_name']) : '';
|
|
28
|
+
$email = isset($_POST['contact_email']) ? sanitize_email($_POST['contact_email']) : '';
|
|
29
|
+
$subject = isset($_POST['contact_subject']) ? sanitize_text_field($_POST['contact_subject']) : '';
|
|
30
|
+
$message = isset($_POST['contact_message']) ? sanitize_textarea_field($_POST['contact_message']) : '';
|
|
31
|
+
|
|
32
|
+
// Get error messages
|
|
33
|
+
$errors = get_transient('contact_form_errors_' . session_id());
|
|
34
|
+
$success = get_transient('contact_form_success_' . session_id());
|
|
35
|
+
|
|
36
|
+
// Clear transients
|
|
37
|
+
delete_transient('contact_form_errors_' . session_id());
|
|
38
|
+
delete_transient('contact_form_success_' . session_id());
|
|
39
|
+
?>
|
|
40
|
+
|
|
41
|
+
<div class="contact-form-wrapper">
|
|
42
|
+
<?php if ($success) : ?>
|
|
43
|
+
<div class="contact-form-success">
|
|
44
|
+
<p><?php echo esc_html($success); ?></p>
|
|
45
|
+
</div>
|
|
46
|
+
<?php endif; ?>
|
|
47
|
+
|
|
48
|
+
<?php if ($errors && is_array($errors)) : ?>
|
|
49
|
+
<div class="contact-form-errors">
|
|
50
|
+
<ul>
|
|
51
|
+
<?php foreach ($errors as $error) : ?>
|
|
52
|
+
<li><?php echo esc_html($error); ?></li>
|
|
53
|
+
<?php endforeach; ?>
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
<?php endif; ?>
|
|
57
|
+
|
|
58
|
+
<form method="post" action="<?php echo esc_url(admin_url('admin-post.php')); ?>" class="contact-form">
|
|
59
|
+
<!-- Nonce field for CSRF protection -->
|
|
60
|
+
<?php wp_nonce_field('contact_form_submit', 'contact_form_nonce'); ?>
|
|
61
|
+
|
|
62
|
+
<!-- Action for WordPress admin-post.php handler -->
|
|
63
|
+
<input type="hidden" name="action" value="submit_contact_form" />
|
|
64
|
+
|
|
65
|
+
<!-- Honeypot field (hidden from users, catches bots) -->
|
|
66
|
+
<input type="text" name="contact_website" value="" style="display:none;" tabindex="-1" autocomplete="off" />
|
|
67
|
+
|
|
68
|
+
<div class="form-field">
|
|
69
|
+
<label for="contact_name">
|
|
70
|
+
<?php _e('Name', 'text-domain'); ?> <span class="required">*</span>
|
|
71
|
+
</label>
|
|
72
|
+
<input type="text"
|
|
73
|
+
id="contact_name"
|
|
74
|
+
name="contact_name"
|
|
75
|
+
value="<?php echo esc_attr($name); ?>"
|
|
76
|
+
required
|
|
77
|
+
maxlength="100" />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="form-field">
|
|
81
|
+
<label for="contact_email">
|
|
82
|
+
<?php _e('Email', 'text-domain'); ?> <span class="required">*</span>
|
|
83
|
+
</label>
|
|
84
|
+
<input type="email"
|
|
85
|
+
id="contact_email"
|
|
86
|
+
name="contact_email"
|
|
87
|
+
value="<?php echo esc_attr($email); ?>"
|
|
88
|
+
required
|
|
89
|
+
maxlength="100" />
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="form-field">
|
|
93
|
+
<label for="contact_subject">
|
|
94
|
+
<?php _e('Subject', 'text-domain'); ?> <span class="required">*</span>
|
|
95
|
+
</label>
|
|
96
|
+
<input type="text"
|
|
97
|
+
id="contact_subject"
|
|
98
|
+
name="contact_subject"
|
|
99
|
+
value="<?php echo esc_attr($subject); ?>"
|
|
100
|
+
required
|
|
101
|
+
maxlength="200" />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div class="form-field">
|
|
105
|
+
<label for="contact_message">
|
|
106
|
+
<?php _e('Message', 'text-domain'); ?> <span class="required">*</span>
|
|
107
|
+
</label>
|
|
108
|
+
<textarea id="contact_message"
|
|
109
|
+
name="contact_message"
|
|
110
|
+
required
|
|
111
|
+
rows="6"
|
|
112
|
+
maxlength="2000"><?php echo esc_textarea($message); ?></textarea>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="form-field">
|
|
116
|
+
<button type="submit" class="submit-button">
|
|
117
|
+
<?php _e('Send Message', 'text-domain'); ?>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
</form>
|
|
121
|
+
</div>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Form Handler Class
|
|
125
|
+
|
|
126
|
+
**File**: `includes/class-contact-form-handler.php`
|
|
127
|
+
|
|
128
|
+
```php
|
|
129
|
+
<?php
|
|
130
|
+
/**
|
|
131
|
+
* Secure contact form handler.
|
|
132
|
+
*
|
|
133
|
+
* Security features:
|
|
134
|
+
* - Nonce verification (CSRF protection)
|
|
135
|
+
* - Capability checks (authorization)
|
|
136
|
+
* - Input sanitization (data cleaning)
|
|
137
|
+
* - Input validation (data verification)
|
|
138
|
+
* - Rate limiting (spam prevention)
|
|
139
|
+
* - Honeypot field (bot detection)
|
|
140
|
+
* - SQL injection prevention (prepared statements)
|
|
141
|
+
* - XSS prevention (output escaping)
|
|
142
|
+
*/
|
|
143
|
+
class Contact_Form_Handler {
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Rate limit: max submissions per IP per hour.
|
|
147
|
+
*/
|
|
148
|
+
const RATE_LIMIT = 3;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initialize the form handler.
|
|
152
|
+
*/
|
|
153
|
+
public function __construct() {
|
|
154
|
+
// Register form submission handler
|
|
155
|
+
add_action('admin_post_submit_contact_form', array($this, 'handle_submission'));
|
|
156
|
+
add_action('admin_post_nopriv_submit_contact_form', array($this, 'handle_submission'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Handle form submission.
|
|
161
|
+
*/
|
|
162
|
+
public function handle_submission() {
|
|
163
|
+
// Start session for error/success messages
|
|
164
|
+
if (!session_id()) {
|
|
165
|
+
session_start();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
$errors = array();
|
|
169
|
+
|
|
170
|
+
// 1. CSRF Protection: Verify nonce
|
|
171
|
+
if (!isset($_POST['contact_form_nonce']) ||
|
|
172
|
+
!wp_verify_nonce($_POST['contact_form_nonce'], 'contact_form_submit')) {
|
|
173
|
+
$errors[] = __('Security check failed. Please try again.', 'text-domain');
|
|
174
|
+
$this->redirect_with_errors($errors);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Bot Detection: Check honeypot field
|
|
179
|
+
if (!empty($_POST['contact_website'])) {
|
|
180
|
+
// Bot detected - silently fail
|
|
181
|
+
$this->redirect_with_success(__('Thank you for your message!', 'text-domain'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 3. Rate Limiting: Check submission rate
|
|
186
|
+
if (!$this->check_rate_limit()) {
|
|
187
|
+
$errors[] = __('Too many submissions. Please try again later.', 'text-domain');
|
|
188
|
+
$this->redirect_with_errors($errors);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 4. Input Sanitization: Clean all inputs
|
|
193
|
+
$name = isset($_POST['contact_name']) ? sanitize_text_field($_POST['contact_name']) : '';
|
|
194
|
+
$email = isset($_POST['contact_email']) ? sanitize_email($_POST['contact_email']) : '';
|
|
195
|
+
$subject = isset($_POST['contact_subject']) ? sanitize_text_field($_POST['contact_subject']) : '';
|
|
196
|
+
$message = isset($_POST['contact_message']) ? sanitize_textarea_field($_POST['contact_message']) : '';
|
|
197
|
+
|
|
198
|
+
// 5. Input Validation: Verify data
|
|
199
|
+
$errors = $this->validate_input($name, $email, $subject, $message);
|
|
200
|
+
|
|
201
|
+
if (!empty($errors)) {
|
|
202
|
+
$this->redirect_with_errors($errors);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 6. Save to Database: Use prepared statements
|
|
207
|
+
$saved = $this->save_to_database($name, $email, $subject, $message);
|
|
208
|
+
|
|
209
|
+
if (!$saved) {
|
|
210
|
+
$errors[] = __('Failed to save message. Please try again.', 'text-domain');
|
|
211
|
+
$this->redirect_with_errors($errors);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 7. Send Email Notification
|
|
216
|
+
$this->send_email_notification($name, $email, $subject, $message);
|
|
217
|
+
|
|
218
|
+
// 8. Log Submission
|
|
219
|
+
$this->log_submission($email);
|
|
220
|
+
|
|
221
|
+
// 9. Success
|
|
222
|
+
$this->redirect_with_success(__('Thank you for your message! We will get back to you soon.', 'text-domain'));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Validate form input.
|
|
227
|
+
*
|
|
228
|
+
* @return array Array of error messages (empty if valid)
|
|
229
|
+
*/
|
|
230
|
+
private function validate_input($name, $email, $subject, $message) {
|
|
231
|
+
$errors = array();
|
|
232
|
+
|
|
233
|
+
// Validate name
|
|
234
|
+
if (empty($name)) {
|
|
235
|
+
$errors[] = __('Name is required.', 'text-domain');
|
|
236
|
+
} elseif (strlen($name) < 2) {
|
|
237
|
+
$errors[] = __('Name must be at least 2 characters.', 'text-domain');
|
|
238
|
+
} elseif (strlen($name) > 100) {
|
|
239
|
+
$errors[] = __('Name must be less than 100 characters.', 'text-domain');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Validate email
|
|
243
|
+
if (empty($email)) {
|
|
244
|
+
$errors[] = __('Email is required.', 'text-domain');
|
|
245
|
+
} elseif (!is_email($email)) {
|
|
246
|
+
$errors[] = __('Please enter a valid email address.', 'text-domain');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Validate subject
|
|
250
|
+
if (empty($subject)) {
|
|
251
|
+
$errors[] = __('Subject is required.', 'text-domain');
|
|
252
|
+
} elseif (strlen($subject) < 3) {
|
|
253
|
+
$errors[] = __('Subject must be at least 3 characters.', 'text-domain');
|
|
254
|
+
} elseif (strlen($subject) > 200) {
|
|
255
|
+
$errors[] = __('Subject must be less than 200 characters.', 'text-domain');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Validate message
|
|
259
|
+
if (empty($message)) {
|
|
260
|
+
$errors[] = __('Message is required.', 'text-domain');
|
|
261
|
+
} elseif (strlen($message) < 10) {
|
|
262
|
+
$errors[] = __('Message must be at least 10 characters.', 'text-domain');
|
|
263
|
+
} elseif (strlen($message) > 2000) {
|
|
264
|
+
$errors[] = __('Message must be less than 2000 characters.', 'text-domain');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return $errors;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Check rate limit for submissions.
|
|
272
|
+
*
|
|
273
|
+
* @return bool True if within rate limit, false otherwise
|
|
274
|
+
*/
|
|
275
|
+
private function check_rate_limit() {
|
|
276
|
+
$ip_address = $this->get_client_ip();
|
|
277
|
+
$transient_key = 'contact_form_rate_' . md5($ip_address);
|
|
278
|
+
|
|
279
|
+
$submissions = get_transient($transient_key);
|
|
280
|
+
|
|
281
|
+
if ($submissions === false) {
|
|
282
|
+
// First submission in this hour
|
|
283
|
+
set_transient($transient_key, 1, HOUR_IN_SECONDS);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if ($submissions >= self::RATE_LIMIT) {
|
|
288
|
+
// Rate limit exceeded
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Increment submission count
|
|
293
|
+
set_transient($transient_key, $submissions + 1, HOUR_IN_SECONDS);
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get client IP address.
|
|
299
|
+
*
|
|
300
|
+
* @return string IP address
|
|
301
|
+
*/
|
|
302
|
+
private function get_client_ip() {
|
|
303
|
+
$ip_address = '';
|
|
304
|
+
|
|
305
|
+
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
|
306
|
+
$ip_address = $_SERVER['HTTP_CLIENT_IP'];
|
|
307
|
+
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
|
308
|
+
$ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
|
309
|
+
} else {
|
|
310
|
+
$ip_address = $_SERVER['REMOTE_ADDR'];
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Sanitize IP address
|
|
314
|
+
return filter_var($ip_address, FILTER_VALIDATE_IP) ? $ip_address : '0.0.0.0';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Save submission to database using prepared statements.
|
|
319
|
+
*
|
|
320
|
+
* @return bool True on success, false on failure
|
|
321
|
+
*/
|
|
322
|
+
private function save_to_database($name, $email, $subject, $message) {
|
|
323
|
+
global $wpdb;
|
|
324
|
+
|
|
325
|
+
$table_name = $wpdb->prefix . 'contact_messages';
|
|
326
|
+
|
|
327
|
+
// Use prepared statement to prevent SQL injection
|
|
328
|
+
$result = $wpdb->insert(
|
|
329
|
+
$table_name,
|
|
330
|
+
array(
|
|
331
|
+
'name' => $name,
|
|
332
|
+
'email' => $email,
|
|
333
|
+
'subject' => $subject,
|
|
334
|
+
'message' => $message,
|
|
335
|
+
'ip_address' => $this->get_client_ip(),
|
|
336
|
+
'submitted_at' => current_time('mysql'),
|
|
337
|
+
'status' => 'unread',
|
|
338
|
+
),
|
|
339
|
+
array(
|
|
340
|
+
'%s', // name
|
|
341
|
+
'%s', // email
|
|
342
|
+
'%s', // subject
|
|
343
|
+
'%s', // message
|
|
344
|
+
'%s', // ip_address
|
|
345
|
+
'%s', // submitted_at
|
|
346
|
+
'%s', // status
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return $result !== false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Send email notification to admin.
|
|
355
|
+
*/
|
|
356
|
+
private function send_email_notification($name, $email, $subject, $message) {
|
|
357
|
+
$admin_email = get_option('admin_email');
|
|
358
|
+
|
|
359
|
+
// Prepare email
|
|
360
|
+
$to = $admin_email;
|
|
361
|
+
$email_subject = sprintf(
|
|
362
|
+
__('[%s] New Contact Form Submission: %s', 'text-domain'),
|
|
363
|
+
get_bloginfo('name'),
|
|
364
|
+
$subject
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// Build email body with escaped content
|
|
368
|
+
$email_body = sprintf(
|
|
369
|
+
__("New contact form submission:\n\nName: %s\nEmail: %s\nSubject: %s\n\nMessage:\n%s", 'text-domain'),
|
|
370
|
+
$name,
|
|
371
|
+
$email,
|
|
372
|
+
$subject,
|
|
373
|
+
$message
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Set headers
|
|
377
|
+
$headers = array(
|
|
378
|
+
'Content-Type: text/plain; charset=UTF-8',
|
|
379
|
+
'Reply-To: ' . $email,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
// Send email
|
|
383
|
+
wp_mail($to, $email_subject, $email_body, $headers);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Log submission for security monitoring.
|
|
388
|
+
*/
|
|
389
|
+
private function log_submission($email) {
|
|
390
|
+
// Log to WordPress debug log if enabled
|
|
391
|
+
if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) {
|
|
392
|
+
error_log(sprintf(
|
|
393
|
+
'Contact form submission from %s (IP: %s)',
|
|
394
|
+
$email,
|
|
395
|
+
$this->get_client_ip()
|
|
396
|
+
));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Redirect back to form with errors.
|
|
402
|
+
*/
|
|
403
|
+
private function redirect_with_errors($errors) {
|
|
404
|
+
set_transient('contact_form_errors_' . session_id(), $errors, 60);
|
|
405
|
+
|
|
406
|
+
$redirect_url = wp_get_referer();
|
|
407
|
+
if (!$redirect_url) {
|
|
408
|
+
$redirect_url = home_url();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
wp_safe_redirect($redirect_url);
|
|
412
|
+
exit;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Redirect back to form with success message.
|
|
417
|
+
*/
|
|
418
|
+
private function redirect_with_success($message) {
|
|
419
|
+
set_transient('contact_form_success_' . session_id(), $message, 60);
|
|
420
|
+
|
|
421
|
+
$redirect_url = wp_get_referer();
|
|
422
|
+
if (!$redirect_url) {
|
|
423
|
+
$redirect_url = home_url();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
wp_safe_redirect($redirect_url);
|
|
427
|
+
exit;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### AJAX Form Handler (Alternative)
|
|
433
|
+
|
|
434
|
+
**File**: `includes/class-contact-form-ajax-handler.php`
|
|
435
|
+
|
|
436
|
+
```php
|
|
437
|
+
<?php
|
|
438
|
+
/**
|
|
439
|
+
* Secure AJAX contact form handler.
|
|
440
|
+
*
|
|
441
|
+
* Additional security for AJAX:
|
|
442
|
+
* - check_ajax_referer() for nonce verification
|
|
443
|
+
* - wp_send_json_error() and wp_send_json_success() for responses
|
|
444
|
+
*/
|
|
445
|
+
class Contact_Form_Ajax_Handler {
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Initialize the AJAX handler.
|
|
449
|
+
*/
|
|
450
|
+
public function __construct() {
|
|
451
|
+
// Register AJAX handlers
|
|
452
|
+
add_action('wp_ajax_submit_contact_form', array($this, 'handle_ajax_submission'));
|
|
453
|
+
add_action('wp_ajax_nopriv_submit_contact_form', array($this, 'handle_ajax_submission'));
|
|
454
|
+
|
|
455
|
+
// Enqueue scripts with localized nonce
|
|
456
|
+
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Enqueue scripts with localized data.
|
|
461
|
+
*/
|
|
462
|
+
public function enqueue_scripts() {
|
|
463
|
+
wp_enqueue_script(
|
|
464
|
+
'contact-form-ajax',
|
|
465
|
+
plugin_dir_url(__FILE__) . '../public/js/contact-form-ajax.js',
|
|
466
|
+
array('jquery'),
|
|
467
|
+
'1.0.0',
|
|
468
|
+
true
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
// Localize script with AJAX URL and nonce
|
|
472
|
+
wp_localize_script('contact-form-ajax', 'contactFormAjax', array(
|
|
473
|
+
'ajax_url' => admin_url('admin-ajax.php'),
|
|
474
|
+
'nonce' => wp_create_nonce('contact_form_ajax_nonce'),
|
|
475
|
+
));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Handle AJAX form submission.
|
|
480
|
+
*/
|
|
481
|
+
public function handle_ajax_submission() {
|
|
482
|
+
// 1. CSRF Protection: Verify AJAX nonce
|
|
483
|
+
if (!check_ajax_referer('contact_form_ajax_nonce', 'nonce', false)) {
|
|
484
|
+
wp_send_json_error(array(
|
|
485
|
+
'message' => __('Security check failed. Please refresh the page and try again.', 'text-domain'),
|
|
486
|
+
));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 2. Bot Detection: Check honeypot
|
|
490
|
+
if (!empty($_POST['contact_website'])) {
|
|
491
|
+
// Bot detected - send fake success
|
|
492
|
+
wp_send_json_success(array(
|
|
493
|
+
'message' => __('Thank you for your message!', 'text-domain'),
|
|
494
|
+
));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 3. Rate Limiting
|
|
498
|
+
if (!$this->check_rate_limit()) {
|
|
499
|
+
wp_send_json_error(array(
|
|
500
|
+
'message' => __('Too many submissions. Please try again later.', 'text-domain'),
|
|
501
|
+
));
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 4. Input Sanitization
|
|
505
|
+
$name = isset($_POST['contact_name']) ? sanitize_text_field($_POST['contact_name']) : '';
|
|
506
|
+
$email = isset($_POST['contact_email']) ? sanitize_email($_POST['contact_email']) : '';
|
|
507
|
+
$subject = isset($_POST['contact_subject']) ? sanitize_text_field($_POST['contact_subject']) : '';
|
|
508
|
+
$message = isset($_POST['contact_message']) ? sanitize_textarea_field($_POST['contact_message']) : '';
|
|
509
|
+
|
|
510
|
+
// 5. Input Validation
|
|
511
|
+
$errors = $this->validate_input($name, $email, $subject, $message);
|
|
512
|
+
|
|
513
|
+
if (!empty($errors)) {
|
|
514
|
+
wp_send_json_error(array(
|
|
515
|
+
'message' => implode(' ', $errors),
|
|
516
|
+
'errors' => $errors,
|
|
517
|
+
));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 6. Save to Database
|
|
521
|
+
$saved = $this->save_to_database($name, $email, $subject, $message);
|
|
522
|
+
|
|
523
|
+
if (!$saved) {
|
|
524
|
+
wp_send_json_error(array(
|
|
525
|
+
'message' => __('Failed to save message. Please try again.', 'text-domain'),
|
|
526
|
+
));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 7. Send Email
|
|
530
|
+
$this->send_email_notification($name, $email, $subject, $message);
|
|
531
|
+
|
|
532
|
+
// 8. Log Submission
|
|
533
|
+
$this->log_submission($email);
|
|
534
|
+
|
|
535
|
+
// 9. Success Response
|
|
536
|
+
wp_send_json_success(array(
|
|
537
|
+
'message' => __('Thank you for your message! We will get back to you soon.', 'text-domain'),
|
|
538
|
+
));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ... (same helper methods as Contact_Form_Handler)
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### JavaScript for AJAX Form
|
|
546
|
+
|
|
547
|
+
**File**: `public/js/contact-form-ajax.js`
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
(function($) {
|
|
551
|
+
'use strict';
|
|
552
|
+
|
|
553
|
+
$(document).ready(function() {
|
|
554
|
+
$('#contact-form-ajax').on('submit', function(e) {
|
|
555
|
+
e.preventDefault();
|
|
556
|
+
|
|
557
|
+
var $form = $(this);
|
|
558
|
+
var $submitButton = $form.find('button[type="submit"]');
|
|
559
|
+
var $messageContainer = $('#form-messages');
|
|
560
|
+
|
|
561
|
+
// Disable submit button
|
|
562
|
+
$submitButton.prop('disabled', true).text('Sending...');
|
|
563
|
+
|
|
564
|
+
// Clear previous messages
|
|
565
|
+
$messageContainer.html('').removeClass('success error');
|
|
566
|
+
|
|
567
|
+
// Prepare form data
|
|
568
|
+
var formData = {
|
|
569
|
+
action: 'submit_contact_form',
|
|
570
|
+
nonce: contactFormAjax.nonce,
|
|
571
|
+
contact_name: $form.find('#contact_name').val(),
|
|
572
|
+
contact_email: $form.find('#contact_email').val(),
|
|
573
|
+
contact_subject: $form.find('#contact_subject').val(),
|
|
574
|
+
contact_message: $form.find('#contact_message').val(),
|
|
575
|
+
contact_website: $form.find('[name="contact_website"]').val() // Honeypot
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// Send AJAX request
|
|
579
|
+
$.ajax({
|
|
580
|
+
url: contactFormAjax.ajax_url,
|
|
581
|
+
type: 'POST',
|
|
582
|
+
data: formData,
|
|
583
|
+
dataType: 'json',
|
|
584
|
+
success: function(response) {
|
|
585
|
+
if (response.success) {
|
|
586
|
+
// Success
|
|
587
|
+
$messageContainer
|
|
588
|
+
.html('<p>' + escapeHtml(response.data.message) + '</p>')
|
|
589
|
+
.addClass('success');
|
|
590
|
+
|
|
591
|
+
// Reset form
|
|
592
|
+
$form[0].reset();
|
|
593
|
+
} else {
|
|
594
|
+
// Error
|
|
595
|
+
$messageContainer
|
|
596
|
+
.html('<p>' + escapeHtml(response.data.message) + '</p>')
|
|
597
|
+
.addClass('error');
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
error: function(xhr, status, error) {
|
|
601
|
+
// AJAX error
|
|
602
|
+
$messageContainer
|
|
603
|
+
.html('<p>An error occurred. Please try again.</p>')
|
|
604
|
+
.addClass('error');
|
|
605
|
+
},
|
|
606
|
+
complete: function() {
|
|
607
|
+
// Re-enable submit button
|
|
608
|
+
$submitButton.prop('disabled', false).text('Send Message');
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Escape HTML to prevent XSS in JavaScript.
|
|
616
|
+
*/
|
|
617
|
+
function escapeHtml(text) {
|
|
618
|
+
var map = {
|
|
619
|
+
'&': '&',
|
|
620
|
+
'<': '<',
|
|
621
|
+
'>': '>',
|
|
622
|
+
'"': '"',
|
|
623
|
+
"'": '''
|
|
624
|
+
};
|
|
625
|
+
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
})(jQuery);
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Database Table Creation
|
|
632
|
+
|
|
633
|
+
**File**: `includes/class-contact-form-database.php`
|
|
634
|
+
|
|
635
|
+
```php
|
|
636
|
+
<?php
|
|
637
|
+
/**
|
|
638
|
+
* Database table management for contact form.
|
|
639
|
+
*/
|
|
640
|
+
class Contact_Form_Database {
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Create the contact messages table.
|
|
644
|
+
*/
|
|
645
|
+
public static function create_table() {
|
|
646
|
+
global $wpdb;
|
|
647
|
+
|
|
648
|
+
$table_name = $wpdb->prefix . 'contact_messages';
|
|
649
|
+
$charset_collate = $wpdb->get_charset_collate();
|
|
650
|
+
|
|
651
|
+
$sql = "CREATE TABLE $table_name (
|
|
652
|
+
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
653
|
+
name varchar(100) NOT NULL,
|
|
654
|
+
email varchar(100) NOT NULL,
|
|
655
|
+
subject varchar(200) NOT NULL,
|
|
656
|
+
message text NOT NULL,
|
|
657
|
+
ip_address varchar(45) NOT NULL,
|
|
658
|
+
submitted_at datetime NOT NULL,
|
|
659
|
+
status varchar(20) NOT NULL DEFAULT 'unread',
|
|
660
|
+
PRIMARY KEY (id),
|
|
661
|
+
KEY email (email),
|
|
662
|
+
KEY status (status),
|
|
663
|
+
KEY submitted_at (submitted_at)
|
|
664
|
+
) $charset_collate;";
|
|
665
|
+
|
|
666
|
+
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
667
|
+
dbDelta($sql);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Admin Page to View Submissions
|
|
673
|
+
|
|
674
|
+
**File**: `admin/class-contact-messages-admin.php`
|
|
675
|
+
|
|
676
|
+
```php
|
|
677
|
+
<?php
|
|
678
|
+
/**
|
|
679
|
+
* Admin page to view contact form submissions.
|
|
680
|
+
*
|
|
681
|
+
* Security features:
|
|
682
|
+
* - Capability check (manage_options)
|
|
683
|
+
* - Nonce verification for actions
|
|
684
|
+
* - Prepared statements for queries
|
|
685
|
+
* - Output escaping
|
|
686
|
+
*/
|
|
687
|
+
class Contact_Messages_Admin {
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Initialize the admin page.
|
|
691
|
+
*/
|
|
692
|
+
public function __construct() {
|
|
693
|
+
add_action('admin_menu', array($this, 'add_admin_menu'));
|
|
694
|
+
add_action('admin_post_delete_contact_message', array($this, 'delete_message'));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Add admin menu item.
|
|
699
|
+
*/
|
|
700
|
+
public function add_admin_menu() {
|
|
701
|
+
add_submenu_page(
|
|
702
|
+
'tools.php',
|
|
703
|
+
__('Contact Messages', 'text-domain'),
|
|
704
|
+
__('Contact Messages', 'text-domain'),
|
|
705
|
+
'manage_options',
|
|
706
|
+
'contact-messages',
|
|
707
|
+
array($this, 'render_admin_page')
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Render the admin page.
|
|
713
|
+
*/
|
|
714
|
+
public function render_admin_page() {
|
|
715
|
+
// Check user capability
|
|
716
|
+
if (!current_user_can('manage_options')) {
|
|
717
|
+
wp_die(__('You do not have sufficient permissions to access this page.', 'text-domain'));
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
global $wpdb;
|
|
721
|
+
$table_name = $wpdb->prefix . 'contact_messages';
|
|
722
|
+
|
|
723
|
+
// Get messages with prepared statement
|
|
724
|
+
$messages = $wpdb->get_results(
|
|
725
|
+
$wpdb->prepare(
|
|
726
|
+
"SELECT * FROM $table_name ORDER BY submitted_at DESC LIMIT %d",
|
|
727
|
+
100
|
|
728
|
+
)
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
?>
|
|
732
|
+
<div class="wrap">
|
|
733
|
+
<h1><?php _e('Contact Messages', 'text-domain'); ?></h1>
|
|
734
|
+
|
|
735
|
+
<?php if (empty($messages)) : ?>
|
|
736
|
+
<p><?php _e('No messages found.', 'text-domain'); ?></p>
|
|
737
|
+
<?php else : ?>
|
|
738
|
+
<table class="wp-list-table widefat fixed striped">
|
|
739
|
+
<thead>
|
|
740
|
+
<tr>
|
|
741
|
+
<th><?php _e('Date', 'text-domain'); ?></th>
|
|
742
|
+
<th><?php _e('Name', 'text-domain'); ?></th>
|
|
743
|
+
<th><?php _e('Email', 'text-domain'); ?></th>
|
|
744
|
+
<th><?php _e('Subject', 'text-domain'); ?></th>
|
|
745
|
+
<th><?php _e('Message', 'text-domain'); ?></th>
|
|
746
|
+
<th><?php _e('Status', 'text-domain'); ?></th>
|
|
747
|
+
<th><?php _e('Actions', 'text-domain'); ?></th>
|
|
748
|
+
</tr>
|
|
749
|
+
</thead>
|
|
750
|
+
<tbody>
|
|
751
|
+
<?php foreach ($messages as $message) : ?>
|
|
752
|
+
<tr>
|
|
753
|
+
<td><?php echo esc_html($message->submitted_at); ?></td>
|
|
754
|
+
<td><?php echo esc_html($message->name); ?></td>
|
|
755
|
+
<td><a href="mailto:<?php echo esc_attr($message->email); ?>"><?php echo esc_html($message->email); ?></a></td>
|
|
756
|
+
<td><?php echo esc_html($message->subject); ?></td>
|
|
757
|
+
<td><?php echo esc_html(wp_trim_words($message->message, 10)); ?></td>
|
|
758
|
+
<td><?php echo esc_html($message->status); ?></td>
|
|
759
|
+
<td>
|
|
760
|
+
<a href="<?php echo esc_url(wp_nonce_url(
|
|
761
|
+
admin_url('admin-post.php?action=delete_contact_message&id=' . $message->id),
|
|
762
|
+
'delete_message_' . $message->id
|
|
763
|
+
)); ?>"
|
|
764
|
+
onclick="return confirm('<?php _e('Are you sure you want to delete this message?', 'text-domain'); ?>');">
|
|
765
|
+
<?php _e('Delete', 'text-domain'); ?>
|
|
766
|
+
</a>
|
|
767
|
+
</td>
|
|
768
|
+
</tr>
|
|
769
|
+
<?php endforeach; ?>
|
|
770
|
+
</tbody>
|
|
771
|
+
</table>
|
|
772
|
+
<?php endif; ?>
|
|
773
|
+
</div>
|
|
774
|
+
<?php
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Delete a message.
|
|
779
|
+
*/
|
|
780
|
+
public function delete_message() {
|
|
781
|
+
// Check user capability
|
|
782
|
+
if (!current_user_can('manage_options')) {
|
|
783
|
+
wp_die(__('You do not have sufficient permissions to perform this action.', 'text-domain'));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Get message ID
|
|
787
|
+
$message_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
|
788
|
+
|
|
789
|
+
if (!$message_id) {
|
|
790
|
+
wp_die(__('Invalid message ID.', 'text-domain'));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Verify nonce
|
|
794
|
+
if (!isset($_GET['_wpnonce']) ||
|
|
795
|
+
!wp_verify_nonce($_GET['_wpnonce'], 'delete_message_' . $message_id)) {
|
|
796
|
+
wp_die(__('Security check failed.', 'text-domain'));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Delete message with prepared statement
|
|
800
|
+
global $wpdb;
|
|
801
|
+
$table_name = $wpdb->prefix . 'contact_messages';
|
|
802
|
+
|
|
803
|
+
$deleted = $wpdb->delete(
|
|
804
|
+
$table_name,
|
|
805
|
+
array('id' => $message_id),
|
|
806
|
+
array('%d')
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
if ($deleted) {
|
|
810
|
+
wp_safe_redirect(admin_url('tools.php?page=contact-messages&deleted=1'));
|
|
811
|
+
} else {
|
|
812
|
+
wp_die(__('Failed to delete message.', 'text-domain'));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
exit;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
## Security Checklist
|
|
821
|
+
|
|
822
|
+
### ✅ CSRF Protection (Cross-Site Request Forgery)
|
|
823
|
+
|
|
824
|
+
- [x] **Nonce field in form**: `wp_nonce_field('contact_form_submit', 'contact_form_nonce')`
|
|
825
|
+
- [x] **Nonce verification in handler**: `wp_verify_nonce($_POST['contact_form_nonce'], 'contact_form_submit')`
|
|
826
|
+
- [x] **AJAX nonce**: `check_ajax_referer('contact_form_ajax_nonce', 'nonce', false)`
|
|
827
|
+
- [x] **Admin action nonce**: `wp_nonce_url()` for delete links
|
|
828
|
+
|
|
829
|
+
### ✅ XSS Prevention (Cross-Site Scripting)
|
|
830
|
+
|
|
831
|
+
- [x] **Output escaping**: `esc_html()`, `esc_attr()`, `esc_url()`, `esc_textarea()`
|
|
832
|
+
- [x] **JavaScript escaping**: Custom `escapeHtml()` function
|
|
833
|
+
- [x] **Email content**: Plain text email (no HTML)
|
|
834
|
+
|
|
835
|
+
### ✅ SQL Injection Prevention
|
|
836
|
+
|
|
837
|
+
- [x] **Prepared statements**: `$wpdb->insert()` with format array
|
|
838
|
+
- [x] **Prepared queries**: `$wpdb->prepare()` with placeholders
|
|
839
|
+
- [x] **Type casting**: `intval()` for IDs
|
|
840
|
+
|
|
841
|
+
### ✅ Authorization
|
|
842
|
+
|
|
843
|
+
- [x] **Capability checks**: `current_user_can('manage_options')`
|
|
844
|
+
- [x] **Admin page protection**: Check capability before rendering
|
|
845
|
+
- [x] **Admin action protection**: Check capability before deleting
|
|
846
|
+
|
|
847
|
+
### ✅ Input Validation
|
|
848
|
+
|
|
849
|
+
- [x] **Required fields**: Check for empty values
|
|
850
|
+
- [x] **Length validation**: Min/max character limits
|
|
851
|
+
- [x] **Email validation**: `is_email()` function
|
|
852
|
+
- [x] **Type validation**: Ensure correct data types
|
|
853
|
+
|
|
854
|
+
### ✅ Input Sanitization
|
|
855
|
+
|
|
856
|
+
- [x] **Text fields**: `sanitize_text_field()`
|
|
857
|
+
- [x] **Email**: `sanitize_email()`
|
|
858
|
+
- [x] **Textarea**: `sanitize_textarea_field()`
|
|
859
|
+
- [x] **IP address**: `filter_var($ip, FILTER_VALIDATE_IP)`
|
|
860
|
+
|
|
861
|
+
### ✅ Spam Prevention
|
|
862
|
+
|
|
863
|
+
- [x] **Rate limiting**: Max 3 submissions per hour per IP
|
|
864
|
+
- [x] **Honeypot field**: Hidden field to catch bots
|
|
865
|
+
- [x] **IP logging**: Track submissions by IP address
|
|
866
|
+
|
|
867
|
+
### ✅ Additional Security
|
|
868
|
+
|
|
869
|
+
- [x] **Session management**: Proper session handling
|
|
870
|
+
- [x] **Transients for messages**: Temporary storage for errors/success
|
|
871
|
+
- [x] **Safe redirects**: `wp_safe_redirect()`
|
|
872
|
+
- [x] **Error logging**: Log submissions for monitoring
|
|
873
|
+
|
|
874
|
+
## Testing Checklist
|
|
875
|
+
|
|
876
|
+
### Functional Testing
|
|
877
|
+
|
|
878
|
+
- [ ] Form displays correctly
|
|
879
|
+
- [ ] Form submits successfully with valid data
|
|
880
|
+
- [ ] Success message displays after submission
|
|
881
|
+
- [ ] Email notification sent to admin
|
|
882
|
+
- [ ] Data saved to database correctly
|
|
883
|
+
- [ ] AJAX form works without page reload
|
|
884
|
+
|
|
885
|
+
### Security Testing
|
|
886
|
+
|
|
887
|
+
- [ ] Form submission fails without nonce
|
|
888
|
+
- [ ] Form submission fails with invalid nonce
|
|
889
|
+
- [ ] Form submission fails with expired nonce
|
|
890
|
+
- [ ] Rate limiting blocks excessive submissions
|
|
891
|
+
- [ ] Honeypot catches bot submissions
|
|
892
|
+
- [ ] Admin page requires manage_options capability
|
|
893
|
+
- [ ] Delete action requires nonce verification
|
|
894
|
+
- [ ] SQL injection attempts are blocked
|
|
895
|
+
- [ ] XSS attempts are escaped
|
|
896
|
+
|
|
897
|
+
### Validation Testing
|
|
898
|
+
|
|
899
|
+
- [ ] Empty fields show error messages
|
|
900
|
+
- [ ] Invalid email shows error message
|
|
901
|
+
- [ ] Too short inputs show error messages
|
|
902
|
+
- [ ] Too long inputs show error messages
|
|
903
|
+
- [ ] Multiple errors display correctly
|
|
904
|
+
|
|
905
|
+
### Edge Cases
|
|
906
|
+
|
|
907
|
+
- [ ] Form works with special characters in input
|
|
908
|
+
- [ ] Form works with Unicode characters
|
|
909
|
+
- [ ] Form works with very long messages
|
|
910
|
+
- [ ] Form handles database errors gracefully
|
|
911
|
+
- [ ] Form handles email sending failures gracefully
|
|
912
|
+
|
|
913
|
+
## Key Takeaways
|
|
914
|
+
|
|
915
|
+
1. **Defense in Depth**: Multiple layers of security (nonces, capability checks, sanitization, escaping)
|
|
916
|
+
2. **Never Trust User Input**: Always sanitize and validate
|
|
917
|
+
3. **Always Escape Output**: Prevent XSS vulnerabilities
|
|
918
|
+
4. **Use Prepared Statements**: Prevent SQL injection
|
|
919
|
+
5. **Rate Limiting**: Prevent spam and abuse
|
|
920
|
+
6. **Honeypot Fields**: Simple bot detection
|
|
921
|
+
7. **Proper Error Handling**: Don't expose sensitive information
|
|
922
|
+
8. **Logging**: Monitor for suspicious activity
|
|
923
|
+
9. **WordPress Functions**: Use built-in security functions
|
|
924
|
+
10. **Testing**: Comprehensive security testing is essential
|
|
925
|
+
|