* @since 0.0.1 */ public function get_endpoints() { return [ // Get currency data for block editor (public endpoint). '/settings' => [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_currency_settings' ], 'permission_callback' => '__return_true', ], // Get and update general settings. '/settings/general' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Get available currencies. '/settings/currencies' => [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_currencies' ], 'permission_callback' => [ $this, 'check_permissions' ], ], // Email notifications are managed per-form via post meta. // See inc/form-editor/assets.php for the form-level email system. // AI settings. '/settings/ai' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_ai_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_ai_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Spam protection settings. '/settings/spam-protection' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_spam_protection_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_spam_protection_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Miscellaneous settings (usage tracking, etc.). '/settings/misc' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_misc_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_misc_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], 'args' => [ 'usage_tracking' => [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], // Donor management settings. '/settings/donor' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_donor_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_donor_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], 'args' => [ 'create_wp_user' => [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', ], // Registered so the handler's (bool) cast receives a real // boolean: a form-encoded "false" would otherwise cast to true // and switch moderation ON when the admin asked for OFF. 'hold_donor_comments' => [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean', ], ], ], ], // Form validation default messages. '/settings/validation' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_validation_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_validation_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Privacy settings (data retention, consent, privacy/terms fields). '/settings/privacy' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_privacy_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_privacy_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Email Reports (weekly donation digest). '/settings/email-reports' => [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_email_reports_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [ $this, 'update_email_reports_settings' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ], // Send this week's report now, to the addresses in the request. '/settings/email-reports/test' => [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'send_test_email_report' ], 'permission_callback' => [ $this, 'check_permissions' ], ], ]; } /** * Get the Email Reports settings (stored values merged over the defaults). * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.6.1 */ public function get_email_reports_settings( $request ) { unset( $request ); // Unused parameter. return new WP_REST_Response( self::email_reports_payload( Email_Reports::get_settings() ), 200 ); } /** * Update the Email Reports settings and (re)schedule the weekly send. * * Turning the report on with no deliverable address is refused with a * 400 rather than quietly stored as off: the client's address check is * looser than is_email(), and a success response would leave the screen * showing the report as on while nothing is scheduled. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.6.1 */ public function update_email_reports_settings( $request ) { $params = $request->get_json_params(); $params = is_array( $params ) ? $params : []; $wants_on = filter_var( $params['enabled'] ?? false, FILTER_VALIDATE_BOOLEAN ); $sanitized = Email_Reports::sanitize( $params ); if ( $wants_on && ! $sanitized['enabled'] ) { return new WP_REST_Response( [ 'success' => false, 'code' => 'no_valid_recipient', 'message' => __( 'Enter at least one valid email address to turn on email reports.', 'suredonation' ), ], 400 ); } return new WP_REST_Response( self::email_reports_payload( Email_Reports::save( $params ) ), 200 ); } /** * The Email Reports response body: the stored settings plus the schedule * state, so the screen can show when the next report goes out, or that * none is queued. * * @param array $settings Stored settings. * @return array * @since 1.6.1 */ private static function email_reports_payload( $settings ) { $next_run = Email_Reports::next_run(); return [ 'success' => true, 'settings' => $settings, 'next_run' => $next_run, 'next_run_label' => null === $next_run ? '' : Helper::get_string_value( wp_date( Helper::get_string_value( get_option( 'date_format' ) ) . ' ' . Helper::get_string_value( get_option( 'time_format' ) ), $next_run ) ), ]; } /** * Send this week's report immediately to the addresses in the request. * * Reads recipients from the request, not the stored settings, so an admin * can preview before saving. Sends even when the week has no donations. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.6.1 */ public function send_test_email_report( $request ) { $params = $request->get_json_params(); $recipients = Email_Reports::parse_recipients( is_array( $params ) ? ( $params['recipients'] ?? '' ) : '' ); if ( [] === $recipients ) { return new WP_REST_Response( [ 'success' => false, 'message' => __( 'Enter at least one valid email address.', 'suredonation' ), ], 400 ); } if ( ! Email_Reports::send_report( $recipients, true ) ) { return new WP_REST_Response( [ 'success' => false, 'message' => __( 'The report could not be sent. Check your site’s email configuration.', 'suredonation' ), ], 500 ); } return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Test report sent.', 'suredonation' ), ], 200 ); } /** * Get the Privacy settings (stored values merged over the defaults). * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.2.0 */ public function get_privacy_settings( $request ) { unset( $request ); // Unused parameter. return new WP_REST_Response( [ 'success' => true, 'settings' => \SureDonation\Inc\Privacy\Privacy_Settings::get_settings(), ], 200 ); } /** * Update the Privacy settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.2.0 */ public function update_privacy_settings( $request ) { $params = $request->get_json_params(); $sanitized = \SureDonation\Inc\Privacy\Privacy_Settings::sanitize( is_array( $params ) ? $params : [] ); Helper::update_suredonation_option( \SureDonation\Inc\Privacy\Privacy_Settings::OPTION_KEY, $sanitized ); return new WP_REST_Response( [ 'success' => true, 'settings' => $sanitized, ], 200 ); } /** * Get the form-validation default messages. * * Returns the stored admin overrides merged over the translatable defaults * so every configurable message always has a value in the editor. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.1.0 */ public function get_validation_settings( $request ) { unset( $request ); // Unused parameter. $defaults = \SureDonation\Inc\Field_Validation::default_validation_messages(); $stored = Helper::get_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, [] ); return new WP_REST_Response( [ 'success' => true, 'settings' => wp_parse_args( is_array( $stored ) ? $stored : [], $defaults ), ], 200 ); } /** * Update the form-validation default messages. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response * @since 1.1.0 */ public function update_validation_settings( $request ) { $current = Helper::get_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, [] ); if ( ! is_array( $current ) ) { $current = []; } /** * Filter the list of allowed validation-message keys. * * Lets extensions register additional message keys for their own field * types, mirroring the `suredonation.settings.tab.validationFields` and * `suredonation.settings.tab.requiredValidationFields` JS filters. * * @since 1.1.0 * @param array $keys Allowed message keys. */ $allowed_keys = apply_filters( 'suredonation_validation_message_keys', array_keys( \SureDonation\Inc\Field_Validation::default_validation_messages() ) ); foreach ( $allowed_keys as $key ) { if ( ! is_string( $key ) ) { continue; } $value = $request->get_param( $key ); // Guard against non-scalar input (array/object) which would make // sanitize_text_field() emit a warning / type error on PHP 8.1+. if ( null !== $value && is_scalar( $value ) ) { $current[ $key ] = sanitize_text_field( (string) $value ); } } Helper::update_suredonation_option( \SureDonation\Inc\Field_Validation::VALIDATION_MESSAGES_OPTION_KEY, $current ); return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Form validation settings saved', 'suredonation' ), ], 200 ); } /** * Get currency settings for block editor. * * Returns minimal currency data needed for frontend/block previews. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 0.0.1 */ public function get_currency_settings( $request ) { unset( $request ); // Unused parameter. $currency = Payment_Helper::get_currency(); return new WP_REST_Response( [ 'currency' => $currency, 'currencySymbol' => Payment_Helper::get_currency_symbol( $currency ), 'isZeroDecimal' => Payment_Helper::is_zero_decimal_currency( $currency ), ], 200 ); } /** * Get general settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 0.0.1 */ public function get_settings( $request ) { unset( $request ); // Unused parameter. $settings = Payment_Helper::get_all_payment_settings(); return new WP_REST_Response( [ 'success' => true, 'settings' => [ 'currency' => $settings['currency'] ?? 'USD', 'payment_mode' => $settings['payment_mode'] ?? 'test', 'currency_sign_position' => Payment_Helper::get_currency_sign_position(), ], ], 200 ); } /** * Update general settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error Response object. * @since 0.0.1 */ public function update_settings( $request ) { $params = $request->get_json_params(); if ( empty( $params ) ) { return new WP_Error( 'invalid_settings', __( 'Invalid settings provided', 'suredonation' ), [ 'status' => 400 ] ); } $current_settings = Payment_Helper::get_all_payment_settings(); // Update currency if provided. if ( isset( $params['currency'] ) ) { $currency = strtoupper( sanitize_text_field( $params['currency'] ) ); // Validate currency. $valid_currencies = array_keys( Payment_Helper::get_all_currencies_data() ); if ( in_array( $currency, $valid_currencies, true ) ) { $current_settings['currency'] = $currency; } } // Update payment mode if provided. if ( isset( $params['payment_mode'] ) ) { $mode = sanitize_text_field( $params['payment_mode'] ); if ( in_array( $mode, [ 'test', 'live' ], true ) ) { $current_settings['payment_mode'] = $mode; } } // Update currency sign position if provided. if ( isset( $params['currency_sign_position'] ) ) { $position = sanitize_text_field( $params['currency_sign_position'] ); if ( in_array( $position, Payment_Helper::ALLOWED_SIGN_POSITIONS, true ) ) { $current_settings['currency_sign_position'] = $position; } } $success = Payment_Helper::update_all_payment_settings( $current_settings ); if ( ! $success ) { return new WP_Error( 'update_failed', __( 'Failed to update settings', 'suredonation' ), [ 'status' => 500 ] ); } return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Settings updated successfully', 'suredonation' ), ], 200 ); } /** * Get available currencies. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 0.0.1 */ public function get_currencies( $request ) { unset( $request ); // Unused parameter. return new WP_REST_Response( [ 'success' => true, 'currencies' => Payment_Helper::get_currencies_list(), ], 200 ); } /** * Get AI settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function get_ai_settings( $request ) { unset( $request ); // Unused parameter. $defaults = [ 'enable_abilities' => false, 'allow_updates' => false, 'allow_delete' => false, 'mcp_server' => false, ]; $settings = Helper::get_suredonation_option( self::AI_OPTION_KEY, [] ); return new WP_REST_Response( [ 'success' => true, 'settings' => wp_parse_args( is_array( $settings ) ? $settings : [], $defaults ), ], 200 ); } /** * Update AI settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function update_ai_settings( $request ) { $params = $request->get_json_params(); $current = Helper::get_suredonation_option( self::AI_OPTION_KEY, [] ); if ( ! is_array( $current ) ) { $current = []; } $allowed = [ 'enable_abilities', 'allow_updates', 'allow_delete', 'mcp_server' ]; foreach ( $allowed as $key ) { if ( isset( $params[ $key ] ) ) { $current[ $key ] = (bool) $params[ $key ]; } } Helper::update_suredonation_option( self::AI_OPTION_KEY, $current ); return new WP_REST_Response( [ 'success' => true, 'message' => __( 'AI settings saved', 'suredonation' ), ], 200 ); } /** * Get spam protection settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.1.0 */ public function get_spam_protection_settings( $request ) { unset( $request ); // Unused parameter. $defaults = [ 'honeypot' => false, ]; $settings = Helper::get_suredonation_option( self::SPAM_OPTION_KEY, [] ); return new WP_REST_Response( [ 'success' => true, 'settings' => wp_parse_args( is_array( $settings ) ? $settings : [], $defaults ), ], 200 ); } /** * Update spam protection settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.1.0 */ public function update_spam_protection_settings( $request ) { $current = Helper::get_suredonation_option( self::SPAM_OPTION_KEY, [] ); if ( ! is_array( $current ) ) { $current = []; } // Read each setting via get_param() so the endpoint accepts JSON, body, // or query params (matches the sibling /settings/* update handlers). $allowed = [ 'honeypot' ]; foreach ( $allowed as $key ) { $value = $request->get_param( $key ); if ( null !== $value ) { $current[ $key ] = (bool) $value; } } Helper::update_suredonation_option( self::SPAM_OPTION_KEY, $current ); return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Spam protection settings saved', 'suredonation' ), ], 200 ); } /** * Get miscellaneous settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function get_misc_settings( $request ) { unset( $request ); // Unused parameter. return new WP_REST_Response( [ 'success' => true, 'settings' => [ // Site option - the BSF Analytics library reads this via // get_site_option(), so the toggle must use the same // scope to stay in sync on multisite. 'usage_tracking' => 'yes' === get_site_option( 'suredonation_usage_optin', false ), ], ], 200 ); } /** * Update miscellaneous settings. * * Stores the usage-tracking opt-in as the standalone 'yes'/'no' * suredonation_usage_optin option read by the BSF Analytics library. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function update_misc_settings( $request ) { $usage_tracking = $request->get_param( 'usage_tracking' ); if ( null !== $usage_tracking ) { if ( $usage_tracking ) { update_site_option( 'suredonation_usage_optin', 'yes' ); } else { // Mirror the library's optout() side effects (see // class-bsf-analytics.php) so the cross-product notice // throttle and the send-check transient stay consistent. update_site_option( 'suredonation_usage_optin', 'no' ); update_site_option( 'bsf_usage_last_displayed_time', time() ); delete_site_transient( 'bsf_usage_track' ); } } return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Settings saved', 'suredonation' ), ], 200 ); } /** * Get donor management settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function get_donor_settings( $request ) { unset( $request ); // Unused parameter. $donor_settings = Helper::get_suredonation_option( self::DONOR_OPTION_KEY, [] ); if ( ! is_array( $donor_settings ) ) { $donor_settings = []; } return new WP_REST_Response( [ 'success' => true, 'settings' => [ // Off by default: guest donations never auto-create WP user accounts. 'create_wp_user' => ! empty( $donor_settings['create_wp_user'] ), // Off by default: donor comments publish as soon as the donation // completes, matching GiveWP and Charitable out of the box. Turning // it on holds new comments as `pending` for review instead. 'hold_donor_comments' => ! empty( $donor_settings['hold_donor_comments'] ), ], ], 200 ); } /** * Update donor management settings. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response Response object. * @since 1.0.0 */ public function update_donor_settings( $request ) { $donor_settings = Helper::get_suredonation_option( self::DONOR_OPTION_KEY, [] ); if ( ! is_array( $donor_settings ) ) { $donor_settings = []; } // Read each setting via get_param() so the endpoint accepts JSON, body, // or query params (matches the sibling /settings/* update handlers). $changed = false; foreach ( [ 'create_wp_user', 'hold_donor_comments' ] as $key ) { $value = $request->get_param( $key ); if ( null !== $value ) { $donor_settings[ $key ] = (bool) $value; $changed = true; } } if ( $changed ) { Helper::update_suredonation_option( self::DONOR_OPTION_KEY, $donor_settings ); } return new WP_REST_Response( [ 'success' => true, 'message' => __( 'Settings saved', 'suredonation' ), ], 200 ); } /** * Check if user has permission to manage settings. * * @return bool True if user has permission. * @since 0.0.1 */ public function check_permissions() { return current_user_can( 'manage_options' ); } }