PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.6.1
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.6.1
1.6.1 1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/api/settings-api.php +548 -295 0.0.1 → 1.6.1 View file →
@@ -6,9 +6,9 @@
6 6 */
7 7
8 8 namespace SureDonation\Inc\API;
9 9
10 -use SureDonation\Inc\Emails\Email_Template;
10 +use SureDonation\Inc\Emails\Email_Reports;
11 11 use SureDonation\Inc\Helper;
12 12 use SureDonation\Inc\Payments\Payment_Helper;
13 13 use WP_Error;
14 14 use WP_REST_Request;
@@ -33,8 +33,29 @@
33 33 */
34 34 public const EMAIL_OPTION_KEY = 'email_notifications';
35 35
36 36 /**
37 + * Option key for AI settings within consolidated options.
38 + *
39 + * @since 1.0.0
40 + */
41 + public const AI_OPTION_KEY = 'ai_settings';
42 +
43 + /**
44 + * Option key for spam protection settings within consolidated options.
45 + *
46 + * @since 1.1.0
47 + */
48 + public const SPAM_OPTION_KEY = 'spam_protection_settings';
49 +
50 + /**
51 + * Option key for donor management settings within consolidated options.
52 + *
53 + * @since 1.0.0
54 + */
55 + public const DONOR_OPTION_KEY = 'donor_settings';
56 +
57 + /**
37 58 * Get settings endpoints.
38 59 *
39 60 * @return array<string, mixed>
40 61 * @since 0.0.1
@@ -41,9 +62,9 @@
41 62 */
42 63 public function get_endpoints() {
43 64 return [
44 65 // Get currency data for block editor (public endpoint).
45 - '/settings' => [
66 + '/settings' => [
46 67 'methods' => WP_REST_Server::READABLE,
47 68 'callback' => [ $this, 'get_currency_settings' ],
48 69 'permission_callback' => '__return_true',
49 70 ],
@@ -48,9 +69,9 @@
48 69 'permission_callback' => '__return_true',
49 70 ],
50 71
51 72 // Get and update general settings.
52 - '/settings/general' => [
73 + '/settings/general' => [
53 74 [
54 75 'methods' => WP_REST_Server::READABLE,
55 76 'callback' => [ $this, 'get_settings' ],
56 77 'permission_callback' => [ $this, 'check_permissions' ],
@@ -62,32 +83,137 @@
62 83 ],
63 84 ],
64 85
65 86 // Get available currencies.
66 - '/settings/currencies' => [
87 + '/settings/currencies' => [
67 88 'methods' => WP_REST_Server::READABLE,
68 89 'callback' => [ $this, 'get_currencies' ],
69 90 'permission_callback' => [ $this, 'check_permissions' ],
70 91 ],
71 92
72 - // Email settings.
73 - '/settings/email' => [
93 + // Email notifications are managed per-form via post meta.
94 + // See inc/form-editor/assets.php for the form-level email system.
95 + // AI settings.
96 + '/settings/ai' => [
74 97 [
75 98 'methods' => WP_REST_Server::READABLE,
76 - 'callback' => [ $this, 'get_email_settings' ],
99 + 'callback' => [ $this, 'get_ai_settings' ],
77 100 'permission_callback' => [ $this, 'check_permissions' ],
78 101 ],
79 102 [
80 103 'methods' => WP_REST_Server::EDITABLE,
81 - 'callback' => [ $this, 'update_email_settings' ],
104 + 'callback' => [ $this, 'update_ai_settings' ],
82 105 'permission_callback' => [ $this, 'check_permissions' ],
83 106 ],
84 107 ],
85 108
86 - // Send test email.
87 - '/settings/email/test' => [
109 + // Spam protection settings.
110 + '/settings/spam-protection' => [
111 + [
112 + 'methods' => WP_REST_Server::READABLE,
113 + 'callback' => [ $this, 'get_spam_protection_settings' ],
114 + 'permission_callback' => [ $this, 'check_permissions' ],
115 + ],
116 + [
117 + 'methods' => WP_REST_Server::EDITABLE,
118 + 'callback' => [ $this, 'update_spam_protection_settings' ],
119 + 'permission_callback' => [ $this, 'check_permissions' ],
120 + ],
121 + ],
122 +
123 + // Miscellaneous settings (usage tracking, etc.).
124 + '/settings/misc' => [
125 + [
126 + 'methods' => WP_REST_Server::READABLE,
127 + 'callback' => [ $this, 'get_misc_settings' ],
128 + 'permission_callback' => [ $this, 'check_permissions' ],
129 + ],
130 + [
131 + 'methods' => WP_REST_Server::EDITABLE,
132 + 'callback' => [ $this, 'update_misc_settings' ],
133 + 'permission_callback' => [ $this, 'check_permissions' ],
134 + 'args' => [
135 + 'usage_tracking' => [
136 + 'type' => 'boolean',
137 + 'sanitize_callback' => 'rest_sanitize_boolean',
138 + ],
139 + ],
140 + ],
141 + ],
142 +
143 + // Donor management settings.
144 + '/settings/donor' => [
145 + [
146 + 'methods' => WP_REST_Server::READABLE,
147 + 'callback' => [ $this, 'get_donor_settings' ],
148 + 'permission_callback' => [ $this, 'check_permissions' ],
149 + ],
150 + [
151 + 'methods' => WP_REST_Server::EDITABLE,
152 + 'callback' => [ $this, 'update_donor_settings' ],
153 + 'permission_callback' => [ $this, 'check_permissions' ],
154 + 'args' => [
155 + 'create_wp_user' => [
156 + 'type' => 'boolean',
157 + 'sanitize_callback' => 'rest_sanitize_boolean',
158 + ],
159 + // Registered so the handler's (bool) cast receives a real
160 + // boolean: a form-encoded "false" would otherwise cast to true
161 + // and switch moderation ON when the admin asked for OFF.
162 + 'hold_donor_comments' => [
163 + 'type' => 'boolean',
164 + 'sanitize_callback' => 'rest_sanitize_boolean',
165 + ],
166 + ],
167 + ],
168 + ],
169 +
170 + // Form validation default messages.
171 + '/settings/validation' => [
172 + [
173 + 'methods' => WP_REST_Server::READABLE,
174 + 'callback' => [ $this, 'get_validation_settings' ],
175 + 'permission_callback' => [ $this, 'check_permissions' ],
176 + ],
177 + [
178 + 'methods' => WP_REST_Server::EDITABLE,
179 + 'callback' => [ $this, 'update_validation_settings' ],
180 + 'permission_callback' => [ $this, 'check_permissions' ],
181 + ],
182 + ],
183 +
184 + // Privacy settings (data retention, consent, privacy/terms fields).
185 + '/settings/privacy' => [
186 + [
187 + 'methods' => WP_REST_Server::READABLE,
188 + 'callback' => [ $this, 'get_privacy_settings' ],
189 + 'permission_callback' => [ $this, 'check_permissions' ],
190 + ],
191 + [
192 + 'methods' => WP_REST_Server::EDITABLE,
193 + 'callback' => [ $this, 'update_privacy_settings' ],
194 + 'permission_callback' => [ $this, 'check_permissions' ],
195 + ],
196 + ],
197 +
198 + // Email Reports (weekly donation digest).
199 + '/settings/email-reports' => [
200 + [
201 + 'methods' => WP_REST_Server::READABLE,
202 + 'callback' => [ $this, 'get_email_reports_settings' ],
203 + 'permission_callback' => [ $this, 'check_permissions' ],
204 + ],
205 + [
206 + 'methods' => WP_REST_Server::EDITABLE,
207 + 'callback' => [ $this, 'update_email_reports_settings' ],
208 + 'permission_callback' => [ $this, 'check_permissions' ],
209 + ],
210 + ],
211 +
212 + // Send this week's report now, to the addresses in the request.
213 + '/settings/email-reports/test' => [
88 214 'methods' => WP_REST_Server::CREATABLE,
89 - 'callback' => [ $this, 'send_test_email' ],
215 + 'callback' => [ $this, 'send_test_email_report' ],
90 216 'permission_callback' => [ $this, 'check_permissions' ],
91 217 ],
92 218 ];
93 219 }
@@ -92,8 +218,238 @@
92 218 ];
93 219 }
94 220
95 221 /**
222 + * Get the Email Reports settings (stored values merged over the defaults).
223 + *
224 + * @param WP_REST_Request $request Request object.
225 + * @return WP_REST_Response
226 + * @since 1.6.1
227 + */
228 + public function get_email_reports_settings( $request ) {
229 + unset( $request ); // Unused parameter.
230 +
231 + return new WP_REST_Response( self::email_reports_payload( Email_Reports::get_settings() ), 200 );
232 + }
233 +
234 + /**
235 + * Update the Email Reports settings and (re)schedule the weekly send.
236 + *
237 + * Turning the report on with no deliverable address is refused with a
238 + * 400 rather than quietly stored as off: the client's address check is
239 + * looser than is_email(), and a success response would leave the screen
240 + * showing the report as on while nothing is scheduled.
241 + *
242 + * @param WP_REST_Request $request Request object.
243 + * @return WP_REST_Response
244 + * @since 1.6.1
245 + */
246 + public function update_email_reports_settings( $request ) {
247 + $params = $request->get_json_params();
248 + $params = is_array( $params ) ? $params : [];
249 +
250 + $wants_on = filter_var( $params['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN );
251 + $sanitized = Email_Reports::sanitize( $params );
252 +
253 + if ( $wants_on && ! $sanitized['enabled'] ) {
254 + return new WP_REST_Response(
255 + [
256 + 'success' => false,
257 + 'code' => 'no_valid_recipient',
258 + 'message' => __( 'Enter at least one valid email address to turn on email reports.', 'suredonation' ),
259 + ],
260 + 400
261 + );
262 + }
263 +
264 + return new WP_REST_Response( self::email_reports_payload( Email_Reports::save( $params ) ), 200 );
265 + }
266 +
267 + /**
268 + * The Email Reports response body: the stored settings plus the schedule
269 + * state, so the screen can show when the next report goes out, or that
270 + * none is queued.
271 + *
272 + * @param array<string, mixed> $settings Stored settings.
273 + * @return array<string, mixed>
274 + * @since 1.6.1
275 + */
276 + private static function email_reports_payload( $settings ) {
277 + $next_run = Email_Reports::next_run();
278 +
279 + return [
280 + 'success' => true,
281 + 'settings' => $settings,
282 + 'next_run' => $next_run,
283 + 'next_run_label' => null === $next_run
284 + ? ''
285 + : Helper::get_string_value( wp_date( Helper::get_string_value( get_option( 'date_format' ) ) . ' ' . Helper::get_string_value( get_option( 'time_format' ) ), $next_run ) ),
286 + ];
287 + }
288 +
289 + /**
290 + * Send this week's report immediately to the addresses in the request.
291 + *
292 + * Reads recipients from the request, not the stored settings, so an admin
293 + * can preview before saving. Sends even when the week has no donations.
294 + *
295 + * @param WP_REST_Request $request Request object.
296 + * @return WP_REST_Response
297 + * @since 1.6.1
298 + */
299 + public function send_test_email_report( $request ) {
300 + $params = $request->get_json_params();
301 + $recipients = Email_Reports::parse_recipients( is_array( $params ) ? ( $params['recipients'] ?? '' ) : '' );
302 +
303 + if ( [] === $recipients ) {
304 + return new WP_REST_Response(
305 + [
306 + 'success' => false,
307 + 'message' => __( 'Enter at least one valid email address.', 'suredonation' ),
308 + ],
309 + 400
310 + );
311 + }
312 +
313 + if ( ! Email_Reports::send_report( $recipients, true ) ) {
314 + return new WP_REST_Response(
315 + [
316 + 'success' => false,
317 + 'message' => __( 'The report could not be sent. Check your site’s email configuration.', 'suredonation' ),
318 + ],
319 + 500
320 + );
321 + }
322 +
323 + return new WP_REST_Response(
324 + [
325 + 'success' => true,
326 + 'message' => __( 'Test report sent.', 'suredonation' ),
327 + ],
328 + 200
329 + );
330 + }
331 +
332 + /**
333 + * Get the Privacy settings (stored values merged over the defaults).
334 + *
335 + * @param WP_REST_Request $request Request object.
336 + * @return WP_REST_Response
337 + * @since 1.2.0
338 + */
339 + public function get_privacy_settings( $request ) {
340 + unset( $request ); // Unused parameter.
341 +
342 + return new WP_REST_Response(
343 + [
344 + 'success' => true,
345 + 'settings' => \SureDonation\Inc\Privacy\Privacy_Settings::get_settings(),
346 + ],
347 + 200
348 + );
349 + }
350 +
351 + /**
352 + * Update the Privacy settings.
353 + *
354 + * @param WP_REST_Request $request Request object.
355 + * @return WP_REST_Response
356 + * @since 1.2.0
357 + */
358 + public function update_privacy_settings( $request ) {
359 + $params = $request->get_json_params();
360 + $sanitized = \SureDonation\Inc\Privacy\Privacy_Settings::sanitize( is_array( $params ) ? $params : [] );
361 +
362 + Helper::update_suredonation_option( \SureDonation\Inc\Privacy\Privacy_Settings::OPTION_KEY, $sanitized );
363 +
364 + return new WP_REST_Response(
365 + [
366 + 'success' => true,
367 + 'settings' => $sanitized,
368 + ],
369 + 200
370 + );
371 + }
372 +
373 + /**
374 + * Get the form-validation default messages.
375 + *
376 + * Returns the stored admin overrides merged over the translatable defaults
377 + * so every configurable message always has a value in the editor.
378 + *
379 + * @param WP_REST_Request $request Request object.
380 + * @return WP_REST_Response
381 + * @since 1.1.0
382 + */
383 + public function get_validation_settings( $request ) {
384 + unset( $request ); // Unused parameter.
385 +
386 + $defaults = \SureDonation\Inc\Field_Validation::default_validation_messages();
387 + $stored = Helper::get_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, [] );
388 +
389 + return new WP_REST_Response(
390 + [
391 + 'success' => true,
392 + 'settings' => wp_parse_args( is_array( $stored ) ? $stored : [], $defaults ),
393 + ],
394 + 200
395 + );
396 + }
397 +
398 + /**
399 + * Update the form-validation default messages.
400 + *
401 + * @param WP_REST_Request $request Request object.
402 + * @return WP_REST_Response
403 + * @since 1.1.0
404 + */
405 + public function update_validation_settings( $request ) {
406 + $current = Helper::get_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, [] );
407 +
408 + if ( ! is_array( $current ) ) {
409 + $current = [];
410 + }
411 +
412 + /**
413 + * Filter the list of allowed validation-message keys.
414 + *
415 + * Lets extensions register additional message keys for their own field
416 + * types, mirroring the `suredonation.settings.tab.validationFields` and
417 + * `suredonation.settings.tab.requiredValidationFields` JS filters.
418 + *
419 + * @since 1.1.0
420 + * @param array<int, string> $keys Allowed message keys.
421 + */
422 + $allowed_keys = apply_filters(
423 + 'suredonation_validation_message_keys',
424 + array_keys( \SureDonation\Inc\Field_Validation::default_validation_messages() )
425 + );
426 +
427 + foreach ( $allowed_keys as $key ) {
428 + if ( ! is_string( $key ) ) {
429 + continue;
430 + }
431 +
432 + $value = $request->get_param( $key );
433 + // Guard against non-scalar input (array/object) which would make
434 + // sanitize_text_field() emit a warning / type error on PHP 8.1+.
435 + if ( null !== $value && is_scalar( $value ) ) {
436 + $current[ $key ] = sanitize_text_field( (string) $value );
437 + }
438 + }
439 +
440 + Helper::update_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, $current );
441 +
442 + return new WP_REST_Response(
443 + [
444 + 'success' => true,
445 + 'message' => __( 'Form validation settings saved', 'suredonation' ),
446 + ],
447 + 200
448 + );
449 + }
450 +
451 + /**
96 452 * Get currency settings for block editor.
97 453 *
98 454 * Returns minimal currency data needed for frontend/block previews.
99 455 *
@@ -131,10 +487,11 @@
131 487 return new WP_REST_Response(
132 488 [
133 489 'success' => true,
134 490 'settings' => [
135 - 'currency' => $settings['currency'] ?? 'USD',
136 - 'payment_mode' => $settings['payment_mode'] ?? 'test',
491 + 'currency' => $settings['currency'] ?? 'USD',
492 + 'payment_mode' => $settings['payment_mode'] ?? 'test',
493 + 'currency_sign_position' => Payment_Helper::get_currency_sign_position(),
137 494 ],
138 495 ],
139 496 200
140 497 );
@@ -178,8 +535,16 @@
178 535 $current_settings['payment_mode'] = $mode;
179 536 }
180 537 }
181 538
539 + // Update currency sign position if provided.
540 + if ( isset( $params['currency_sign_position'] ) ) {
541 + $position = sanitize_text_field( $params['currency_sign_position'] );
542 + if ( in_array( $position, Payment_Helper::ALLOWED_SIGN_POSITIONS, true ) ) {
543 + $current_settings['currency_sign_position'] = $position;
544 + }
545 + }
546 +
182 547 $success = Payment_Helper::update_all_payment_settings( $current_settings );
183 548
184 549 if ( ! $success ) {
185 550 return new WP_Error(
@@ -207,20 +572,12 @@
207 572 */
208 573 public function get_currencies( $request ) {
209 574 unset( $request ); // Unused parameter.
210 575
211 - $currencies_data = Payment_Helper::get_all_currencies_data();
212 -
213 - // Format for frontend: "CODE - Name".
214 - $currencies = [];
215 - foreach ( $currencies_data as $code => $data ) {
216 - $currencies[ $code ] = $code . ' - ' . $data['name'];
217 - }
218 -
219 576 return new WP_REST_Response(
220 577 [
221 578 'success' => true,
222 - 'currencies' => $currencies,
579 + 'currencies' => Payment_Helper::get_currencies_list(),
223 580 ],
224 581 200
225 582 );
226 583 }
@@ -225,34 +582,29 @@
225 582 );
226 583 }
227 584
228 585 /**
229 - * Get email settings.
586 + * Get AI settings.
230 587 *
231 588 * @param WP_REST_Request $request Request object.
232 589 * @return WP_REST_Response Response object.
233 - * @since 0.0.1
590 + * @since 1.0.0
234 591 */
235 - public function get_email_settings( $request ) {
592 + public function get_ai_settings( $request ) {
236 593 unset( $request ); // Unused parameter.
237 594
238 - $saved_notifications = Helper::get_array_value( Helper::get_suredonation_option( self::EMAIL_OPTION_KEY, [] ) );
239 - $default_notifications = $this->get_default_notifications();
595 + $defaults = [
596 + 'enable_abilities' => false,
597 + 'allow_updates' => false,
598 + 'allow_delete' => false,
599 + 'mcp_server' => false,
600 + ];
601 + $settings = Helper::get_suredonation_option( self::AI_OPTION_KEY, [] );
240 602
241 - // Merge saved settings with defaults.
242 - $notifications = [];
243 - foreach ( $default_notifications as $key => $default ) {
244 - $saved_value = isset( $saved_notifications[ $key ] ) && is_array( $saved_notifications[ $key ] ) ? $saved_notifications[ $key ] : [];
245 - $notifications[ $key ] = wp_parse_args(
246 - $saved_value,
247 - $default
248 - );
249 - }
250 -
251 603 return new WP_REST_Response(
252 604 [
253 - 'success' => true,
254 - 'notifications' => $notifications,
605 + 'success' => true,
606 + 'settings' => wp_parse_args( is_array( $settings ) ? $settings : [], $defaults ),
255 607 ],
256 608 200
257 609 );
258 610 }
@@ -257,50 +609,35 @@
257 609 );
258 610 }
259 611
260 612 /**
261 - * Update email settings.
613 + * Update AI settings.
262 614 *
263 615 * @param WP_REST_Request $request Request object.
264 - * @return WP_REST_Response|WP_Error Response object.
265 - * @since 0.0.1
616 + * @return WP_REST_Response Response object.
617 + * @since 1.0.0
266 618 */
267 - public function update_email_settings( $request ) {
268 - $params = $request->get_json_params();
619 + public function update_ai_settings( $request ) {
620 + $params = $request->get_json_params();
621 + $current = Helper::get_suredonation_option( self::AI_OPTION_KEY, [] );
269 622
270 - if ( empty( $params ) || ! isset( $params['notifications'] ) ) {
271 - return new WP_Error(
272 - 'invalid_settings',
273 - __( 'Invalid settings provided', 'suredonation' ),
274 - [ 'status' => 400 ]
275 - );
623 + if ( ! is_array( $current ) ) {
624 + $current = [];
276 625 }
277 626
278 - $current_notifications = Helper::get_array_value( Helper::get_suredonation_option( self::EMAIL_OPTION_KEY, [] ) );
279 - $valid_keys = array_keys( $this->get_default_notifications() );
280 -
281 - foreach ( $params['notifications'] as $key => $notification ) {
282 - // Only allow known notification types.
283 - if ( ! in_array( $key, $valid_keys, true ) ) {
284 - continue;
627 + $allowed = [ 'enable_abilities', 'allow_updates', 'allow_delete', 'mcp_server' ];
628 + foreach ( $allowed as $key ) {
629 + if ( isset( $params[ $key ] ) ) {
630 + $current[ $key ] = (bool) $params[ $key ];
285 631 }
286 -
287 - $current_notifications[ $key ] = [
288 - 'enabled' => isset( $notification['enabled'] ) ? (bool) $notification['enabled'] : false,
289 - 'subject' => isset( $notification['subject'] ) ? sanitize_text_field( $notification['subject'] ) : '',
290 - 'from_name' => isset( $notification['from_name'] ) ? sanitize_text_field( $notification['from_name'] ) : '',
291 - 'from_email' => isset( $notification['from_email'] ) ? sanitize_email( $notification['from_email'] ) : '',
292 - 'reply_to' => isset( $notification['reply_to'] ) ? sanitize_email( $notification['reply_to'] ) : '',
293 - 'email_body' => isset( $notification['email_body'] ) ? wp_kses_post( $notification['email_body'] ) : '',
294 - ];
295 632 }
296 633
297 - Helper::update_suredonation_option( self::EMAIL_OPTION_KEY, $current_notifications );
634 + Helper::update_suredonation_option( self::AI_OPTION_KEY, $current );
298 635
299 636 return new WP_REST_Response(
300 637 [
301 638 'success' => true,
302 - 'message' => __( 'Email settings saved', 'suredonation' ),
639 + 'message' => __( 'AI settings saved', 'suredonation' ),
303 640 ],
304 641 200
305 642 );
306 643 }
@@ -305,124 +642,61 @@
305 642 );
306 643 }
307 644
308 645 /**
309 - * Send a test email.
646 + * Get spam protection settings.
310 647 *
311 648 * @param WP_REST_Request $request Request object.
312 - * @return WP_REST_Response|WP_Error Response object.
313 - * @since 0.0.1
649 + * @return WP_REST_Response Response object.
650 + * @since 1.1.0
314 651 */
315 - public function send_test_email( $request ) {
316 - $params = $request->get_json_params();
652 + public function get_spam_protection_settings( $request ) {
653 + unset( $request ); // Unused parameter.
317 654
318 - if ( empty( $params['notification_id'] ) ) {
319 - return new WP_Error(
320 - 'missing_notification_id',
321 - __( 'Notification ID is required', 'suredonation' ),
322 - [ 'status' => 400 ]
323 - );
324 - }
655 + $defaults = [
656 + 'honeypot' => false,
657 + ];
658 + $settings = Helper::get_suredonation_option( self::SPAM_OPTION_KEY, [] );
325 659
326 - $notification_id = sanitize_text_field( $params['notification_id'] );
327 - $admin_email = get_option( 'admin_email' );
328 - $test_email = ! empty( $params['test_email'] ) ? sanitize_email( $params['test_email'] ) : ( is_string( $admin_email ) ? $admin_email : '' );
660 + return new WP_REST_Response(
661 + [
662 + 'success' => true,
663 + 'settings' => wp_parse_args( is_array( $settings ) ? $settings : [], $defaults ),
664 + ],
665 + 200
666 + );
667 + }
329 668
330 - if ( ! is_email( $test_email ) ) {
331 - return new WP_Error(
332 - 'invalid_email',
333 - __( 'Invalid email address', 'suredonation' ),
334 - [ 'status' => 400 ]
335 - );
336 - }
669 + /**
670 + * Update spam protection settings.
671 + *
672 + * @param WP_REST_Request $request Request object.
673 + * @return WP_REST_Response Response object.
674 + * @since 1.1.0
675 + */
676 + public function update_spam_protection_settings( $request ) {
677 + $current = Helper::get_suredonation_option( self::SPAM_OPTION_KEY, [] );
337 678
338 - // Get notification settings.
339 - $saved_notifications = Helper::get_array_value( Helper::get_suredonation_option( self::EMAIL_OPTION_KEY, [] ) );
340 - $default_notifications = $this->get_default_notifications();
341 -
342 - if ( ! isset( $default_notifications[ $notification_id ] ) ) {
343 - return new WP_Error(
344 - 'invalid_notification',
345 - __( 'Invalid notification type', 'suredonation' ),
346 - [ 'status' => 400 ]
347 - );
679 + if ( ! is_array( $current ) ) {
680 + $current = [];
348 681 }
349 682
350 - $saved_notification_value = isset( $saved_notifications[ $notification_id ] ) && is_array( $saved_notifications[ $notification_id ] ) ? $saved_notifications[ $notification_id ] : [];
351 - $notification = wp_parse_args(
352 - $saved_notification_value,
353 - $default_notifications[ $notification_id ]
354 - );
355 -
356 - // Use current form data if provided (for preview before save).
357 - if ( ! empty( $params['notification_data'] ) && is_array( $params['notification_data'] ) ) {
358 - $raw = $params['notification_data'];
359 - $sanitized_data = [];
360 - if ( isset( $raw['subject'] ) ) {
361 - $sanitized_data['subject'] = sanitize_text_field( $raw['subject'] );
683 + // Read each setting via get_param() so the endpoint accepts JSON, body,
684 + // or query params (matches the sibling /settings/* update handlers).
685 + $allowed = [ 'honeypot' ];
686 + foreach ( $allowed as $key ) {
687 + $value = $request->get_param( $key );
688 + if ( null !== $value ) {
689 + $current[ $key ] = (bool) $value;
362 690 }
363 - if ( isset( $raw['from_name'] ) ) {
364 - $sanitized_data['from_name'] = sanitize_text_field( $raw['from_name'] );
365 - }
366 - if ( isset( $raw['from_email'] ) ) {
367 - $sanitized_data['from_email'] = sanitize_email( $raw['from_email'] );
368 - }
369 - if ( isset( $raw['reply_to'] ) ) {
370 - $sanitized_data['reply_to'] = sanitize_email( $raw['reply_to'] );
371 - }
372 - if ( isset( $raw['email_body'] ) ) {
373 - $sanitized_data['email_body'] = wp_kses_post( $raw['email_body'] );
374 - }
375 - if ( isset( $raw['enabled'] ) ) {
376 - $sanitized_data['enabled'] = (bool) $raw['enabled'];
377 - }
378 - $notification = wp_parse_args( $sanitized_data, $notification );
379 691 }
380 692
381 - // Create sample donation data for smart tags.
382 - $sample_data = $this->get_sample_donation_data();
693 + Helper::update_suredonation_option( self::SPAM_OPTION_KEY, $current );
383 694
384 - // Process smart tags.
385 - $subject = $this->process_test_smart_tags( $notification['subject'] ?? '', $sample_data );
386 - $email_body = $this->process_test_smart_tags( $notification['email_body'] ?? '', $sample_data );
387 -
388 - // Get from name and email.
389 - $from_name = ! empty( $notification['from_name'] ) ? $notification['from_name'] : get_bloginfo( 'name' );
390 - $from_email = ! empty( $notification['from_email'] ) ? $notification['from_email'] : get_option( 'admin_email' );
391 - $reply_to = ! empty( $notification['reply_to'] ) ? $notification['reply_to'] : $from_email;
392 -
393 - // Process smart tags in from name.
394 - $from_name = $this->process_test_smart_tags( $from_name, $sample_data );
395 -
396 - // Set email headers.
397 - $headers = [
398 - 'Content-Type: text/html; charset=UTF-8',
399 - sprintf( 'From: %s <%s>', $from_name, $from_email ),
400 - sprintf( 'Reply-To: %s', $reply_to ),
401 - ];
402 -
403 - // Format email body with HTML wrapper.
404 - $email_body = $this->format_test_email_body( $email_body );
405 -
406 - // Send email.
407 - $sent = wp_mail( $test_email, $subject, $email_body, $headers );
408 -
409 - if ( ! $sent ) {
410 - return new WP_Error(
411 - 'email_failed',
412 - __( 'Failed to send test email. Please check your email configuration.', 'suredonation' ),
413 - [ 'status' => 500 ]
414 - );
415 - }
416 -
417 695 return new WP_REST_Response(
418 696 [
419 697 'success' => true,
420 - 'message' => sprintf(
421 - /* translators: %s: email address */
422 - __( 'Test email sent to %s', 'suredonation' ),
423 - $test_email
424 - ),
698 + 'message' => __( 'Spam protection settings saved', 'suredonation' ),
425 699 ],
426 700 200
427 701 );
428 702 }
@@ -427,161 +701,140 @@
427 701 );
428 702 }
429 703
430 704 /**
431 - * Check if user has permission to manage settings.
705 + * Get miscellaneous settings.
432 706 *
433 - * @return bool True if user has permission.
434 - * @since 0.0.1
707 + * @param WP_REST_Request $request Request object.
708 + * @return WP_REST_Response Response object.
709 + * @since 1.0.0
435 710 */
436 - public function check_permissions() {
437 - return current_user_can( 'manage_options' );
711 + public function get_misc_settings( $request ) {
712 + unset( $request ); // Unused parameter.
713 +
714 + return new WP_REST_Response(
715 + [
716 + 'success' => true,
717 + 'settings' => [
718 + // Site option - the BSF Analytics library reads this via
719 + // get_site_option(), so the toggle must use the same
720 + // scope to stay in sync on multisite.
721 + 'usage_tracking' => 'yes' === get_site_option( 'suredonation_usage_optin', false ),
722 + ],
723 + ],
724 + 200
725 + );
438 726 }
439 727
440 728 /**
441 - * Get default notification configurations.
729 + * Update miscellaneous settings.
442 730 *
443 - * @return array<string, array<string, mixed>> Default notifications.
444 - * @since 0.0.1
731 + * Stores the usage-tracking opt-in as the standalone 'yes'/'no'
732 + * suredonation_usage_optin option read by the BSF Analytics library.
733 + *
734 + * @param WP_REST_Request $request Request object.
735 + * @return WP_REST_Response Response object.
736 + * @since 1.0.0
445 737 */
446 - private function get_default_notifications() {
447 - return [
448 - 'donation_receipt' => [
449 - 'id' => 'donation_receipt',
450 - 'name' => __( 'Donation Receipt', 'suredonation' ),
451 - 'description' => __( 'Sent to donor after a successful donation.', 'suredonation' ),
452 - 'recipient' => 'donor',
453 - 'enabled' => true,
454 - 'subject' => __( 'Thank you for your donation!', 'suredonation' ),
455 - 'from_name' => '',
456 - 'from_email' => '',
457 - 'reply_to' => '',
458 - 'email_body' => $this->get_donation_receipt_body(),
738 + public function update_misc_settings( $request ) {
739 + $usage_tracking = $request->get_param( 'usage_tracking' );
740 +
741 + if ( null !== $usage_tracking ) {
742 + if ( $usage_tracking ) {
743 + update_site_option( 'suredonation_usage_optin', 'yes' );
744 + } else {
745 + // Mirror the library's optout() side effects (see
746 + // class-bsf-analytics.php) so the cross-product notice
747 + // throttle and the send-check transient stay consistent.
748 + update_site_option( 'suredonation_usage_optin', 'no' );
749 + update_site_option( 'bsf_usage_last_displayed_time', time() );
750 + delete_site_transient( 'bsf_usage_track' );
751 + }
752 + }
753 +
754 + return new WP_REST_Response(
755 + [
756 + 'success' => true,
757 + 'message' => __( 'Settings saved', 'suredonation' ),
459 758 ],
460 - 'admin_new_donation' => [
461 - 'id' => 'admin_new_donation',
462 - 'name' => __( 'New Donation (Admin)', 'suredonation' ),
463 - 'description' => __( 'Sent to admin when a new donation is received.', 'suredonation' ),
464 - 'recipient' => 'admin',
465 - 'enabled' => true,
466 - 'subject' => __( 'New donation received!', 'suredonation' ),
467 - 'from_name' => '',
468 - 'from_email' => '',
469 - 'reply_to' => '',
470 - 'email_body' => $this->get_admin_new_donation_body(),
471 - ],
472 - ];
759 + 200
760 + );
473 761 }
474 762
475 763 /**
476 - * Get donation receipt email body.
764 + * Get donor management settings.
477 765 *
478 - * @return string Email body in HTML format.
479 - * @since 0.0.1
766 + * @param WP_REST_Request $request Request object.
767 + * @return WP_REST_Response Response object.
768 + * @since 1.0.0
480 769 */
481 - private function get_donation_receipt_body() {
482 - ob_start();
483 - ?>
484 - <p><strong><span style="font-size: 18px;"><?php esc_html_e( 'Thank You for Your Donation!', 'suredonation' ); ?></span></strong></p>
485 - <p><?php esc_html_e( 'Dear', 'suredonation' ); ?> {donor_name},</p>
486 - <p><?php esc_html_e( 'Thank you for your generous donation of', 'suredonation' ); ?> <strong>{amount}</strong> <?php esc_html_e( 'to', 'suredonation' ); ?> <strong>{campaign_name}</strong>.</p>
487 - <p><?php esc_html_e( 'Your support means the world to us and helps us continue our mission.', 'suredonation' ); ?></p>
488 - <p><strong><?php esc_html_e( 'Donation Details:', 'suredonation' ); ?></strong></p>
489 - <ul>
490 - <li><?php esc_html_e( 'Amount:', 'suredonation' ); ?> {amount}</li>
491 - <li><?php esc_html_e( 'Campaign:', 'suredonation' ); ?> {campaign_name}</li>
492 - <li><?php esc_html_e( 'Date:', 'suredonation' ); ?> {donation_date}</li>
493 - <li><?php esc_html_e( 'Transaction ID:', 'suredonation' ); ?> {transaction_id}</li>
494 - </ul>
495 - <p><?php esc_html_e( 'Best regards,', 'suredonation' ); ?><br />{site_title}</p>
496 - <?php
497 - $output = ob_get_clean();
498 - return trim( false !== $output ? $output : '' );
499 - }
770 + public function get_donor_settings( $request ) {
771 + unset( $request ); // Unused parameter.
500 772
501 - /**
502 - * Get admin new donation email body.
503 - *
504 - * @return string Email body in HTML format.
505 - * @since 0.0.1
506 - */
507 - private function get_admin_new_donation_body() {
508 - ob_start();
509 - ?>
510 - <p><strong><span style="font-size: 18px;"><?php esc_html_e( 'New Donation Received!', 'suredonation' ); ?></span></strong></p>
511 - <p><?php esc_html_e( 'A new donation has been received for your campaign.', 'suredonation' ); ?></p>
512 - <p><strong><?php esc_html_e( 'Donation Details:', 'suredonation' ); ?></strong></p>
513 - <ul>
514 - <li><?php esc_html_e( 'Donor:', 'suredonation' ); ?> {donor_name} ({donor_email})</li>
515 - <li><?php esc_html_e( 'Amount:', 'suredonation' ); ?> {amount}</li>
516 - <li><?php esc_html_e( 'Campaign:', 'suredonation' ); ?> {campaign_name}</li>
517 - <li><?php esc_html_e( 'Date:', 'suredonation' ); ?> {donation_date}</li>
518 - <li><?php esc_html_e( 'Transaction ID:', 'suredonation' ); ?> {transaction_id}</li>
519 - </ul>
520 - <?php
521 - $output = ob_get_clean();
522 - return trim( false !== $output ? $output : '' );
773 + $donor_settings = Helper::get_suredonation_option( self::DONOR_OPTION_KEY, [] );
774 + if ( ! is_array( $donor_settings ) ) {
775 + $donor_settings = [];
776 + }
777 +
778 + return new WP_REST_Response(
779 + [
780 + 'success' => true,
781 + 'settings' => [
782 + // Off by default: guest donations never auto-create WP user accounts.
783 + 'create_wp_user' => ! empty( $donor_settings['create_wp_user'] ),
784 + // Off by default: donor comments publish as soon as the donation
785 + // completes, matching GiveWP and Charitable out of the box. Turning
786 + // it on holds new comments as `pending` for review instead.
787 + 'hold_donor_comments' => ! empty( $donor_settings['hold_donor_comments'] ),
788 + ],
789 + ],
790 + 200
791 + );
523 792 }
524 793
525 794 /**
526 - * Get sample donation data for test emails.
795 + * Update donor management settings.
527 796 *
528 - * @return array<string, int|string> Sample data.
529 - * @since 0.0.1
797 + * @param WP_REST_Request $request Request object.
798 + * @return WP_REST_Response Response object.
799 + * @since 1.0.0
530 800 */
531 - private function get_sample_donation_data() {
532 - $currency = Payment_Helper::get_currency();
533 - $currency_symbol = Payment_Helper::get_currency_symbol( $currency );
534 - $date_format = get_option( 'date_format' );
535 - $admin_email = get_option( 'admin_email' );
801 + public function update_donor_settings( $request ) {
802 + $donor_settings = Helper::get_suredonation_option( self::DONOR_OPTION_KEY, [] );
803 + if ( ! is_array( $donor_settings ) ) {
804 + $donor_settings = [];
805 + }
536 806
537 - return [
538 - 'donor_name' => __( 'John Doe', 'suredonation' ),
539 - 'donor_email' => '[email protected]',
540 - 'amount' => $currency_symbol . '50.00',
541 - 'campaign_name' => __( 'Sample Campaign', 'suredonation' ),
542 - 'donation_date' => current_time( is_string( $date_format ) ? $date_format : 'Y-m-d' ),
543 - 'transaction_id' => 'pi_test_' . wp_generate_password( 16, false ),
544 - 'site_title' => get_bloginfo( 'name' ),
545 - 'admin_email' => is_string( $admin_email ) ? $admin_email : '',
546 - 'site_url' => home_url(),
547 - 'admin_url' => admin_url( 'admin.php?page=suredonation' ),
548 - ];
549 - }
807 + // Read each setting via get_param() so the endpoint accepts JSON, body,
808 + // or query params (matches the sibling /settings/* update handlers).
809 + $changed = false;
810 + foreach ( [ 'create_wp_user', 'hold_donor_comments' ] as $key ) {
811 + $value = $request->get_param( $key );
812 + if ( null !== $value ) {
813 + $donor_settings[ $key ] = (bool) $value;
814 + $changed = true;
815 + }
816 + }
550 817
551 - /**
552 - * Process smart tags for test emails.
553 - *
554 - * @param string $content Content with smart tags.
555 - * @param array<string, int|string> $sample_data Sample data for replacement.
556 - * @return string Processed content.
557 - * @since 0.0.1
558 - */
559 - private function process_test_smart_tags( $content, $sample_data ) {
560 - $tags = [
561 - '{donor_name}' => $sample_data['donor_name'],
562 - '{donor_email}' => $sample_data['donor_email'],
563 - '{amount}' => $sample_data['amount'],
564 - '{campaign_name}' => $sample_data['campaign_name'],
565 - '{donation_date}' => $sample_data['donation_date'],
566 - '{transaction_id}' => $sample_data['transaction_id'],
567 - '{site_title}' => $sample_data['site_title'],
568 - '{admin_email}' => $sample_data['admin_email'],
569 - '{site_url}' => $sample_data['site_url'],
570 - '{admin_url}' => $sample_data['admin_url'],
571 - ];
818 + if ( $changed ) {
819 + Helper::update_suredonation_option( self::DONOR_OPTION_KEY, $donor_settings );
820 + }
572 821
573 - return str_replace( array_keys( $tags ), array_values( $tags ), $content );
822 + return new WP_REST_Response(
823 + [
824 + 'success' => true,
825 + 'message' => __( 'Settings saved', 'suredonation' ),
826 + ],
827 + 200
828 + );
574 829 }
575 830
576 831 /**
577 - * Format test email body with HTML wrapper.
832 + * Check if user has permission to manage settings.
578 833 *
579 - * @param string $body Email body content.
580 - * @return string Formatted HTML email.
834 + * @return bool True if user has permission.
581 835 * @since 0.0.1
582 836 */
583 - private function format_test_email_body( $body ) {
584 - $email_template = Email_Template::get_instance();
585 - return $email_template->render( $body );
837 + public function check_permissions() {
838 + return current_user_can( 'manage_options' );
586 839 }
587 840 }