PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.2
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / admin / settings / class-charitable-settings.php

class-charitable-settings.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.12.2, at includes/admin/settings/class-charitable-settings.php

1,400 lines 57.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Settings UI.
4 *
5 * @package Charitable/Classes/Charitable_Settings
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 * @version 1.0.0
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Settings' ) ) :
19
20 /**
21 * Charitable_Settings
22 *
23 * @final
24 * @since 1.0.0
25 */
26 final class Charitable_Settings {
27
28 /**
29 * The single instance of this class.
30 *
31 * @var Charitable_Settings|null
32 */
33 private static $instance = null;
34
35 /**
36 * Dynamic groups.
37 *
38 * @since 1.5.7
39 *
40 * @var array
41 */
42 private $dynamic_groups;
43
44 /**
45 * List of static pages, used in some settings.
46 *
47 * @since 1.5.9
48 *
49 * @var array
50 */
51 private $pages;
52
53 /**
54 * Create object instance.
55 *
56 * @since 1.0.0
57 */
58 private function __construct() {
59 do_action( 'charitable_admin_settings_start', $this );
60
61 add_action( 'charitable_after_admin_settings', array( $this, 'settings_cta' ) );
62 }
63
64 /**
65 * Returns and/or create the single instance of this class.
66 *
67 * @since 1.2.0
68 *
69 * @return Charitable_Settings
70 */
71 public static function get_instance() {
72 if ( is_null( self::$instance ) ) {
73 self::$instance = new self();
74 }
75
76 return self::$instance;
77 }
78
79 /**
80 * Return the array of tabs used on the settings page.
81 *
82 * @since 1.0.0
83 *
84 * @return string[]
85 */
86 public function get_sections() {
87 /**
88 * Filter the settings tabs.
89 *
90 * @since 1.0.0
91 * @version 1.8.1.6 removed 'tools' tab
92 * @version 1.8.5.1 added marketing and security tabs depending on addon status.
93 *
94 * @param string[] $tabs List of tabs in key=>label format.
95 */
96 $tabs = array(
97 'general' => __( 'General', 'charitable' ),
98 'gateways' => __( 'Payment Gateways', 'charitable' ),
99 'emails' => __( 'Emails', 'charitable' ),
100 'donors' => __( 'Donors', 'charitable' ),
101 'privacy' => __( 'Privacy', 'charitable' ),
102 'advanced' => __( 'Advanced', 'charitable' ),
103 );
104
105 // Always show marketing tab. Sub-tabs (Newsletters, Conversion Tracking, Google Analytics)
106 // handle the per-feature display: Newsletter Connect addon (which works with Lite) renders
107 // its real UI under the Newsletters sub-tab when active; Conversion Tracking and Google
108 // Analytics always render Pro CTAs in Lite.
109 $tabs = charitable_add_settings_tab(
110 $tabs,
111 'marketing',
112 __( 'Marketing', 'charitable' ),
113 array(
114 'index' => 5,
115 )
116 );
117
118 // Always show security tab.
119 // When spam blocker is active, it will handle the settings.
120 // When spam blocker is inactive, core security features will handle the settings.
121 $tabs = charitable_add_settings_tab(
122 $tabs,
123 'security',
124 __( 'Security', 'charitable' ),
125 array(
126 'index' => 6,
127 )
128 );
129
130 // Ensure the advanced tab is always last by moving it to the end of the array.
131 if ( isset( $tabs['advanced'] ) ) {
132 $advanced_tab = $tabs['advanced'];
133 unset( $tabs['advanced'] );
134 $tabs['advanced'] = $advanced_tab;
135 }
136
137 return apply_filters( 'charitable_settings_tabs', $tabs );
138 }
139
140 /**
141 * Return the array of sub tabs used on the Marketing settings page.
142 *
143 * Each label carries an inline "Pro" badge:
144 * - Newsletters: the Newsletter Connect addon works on Lite, but the badge
145 * still appears since the bulk of newsletter functionality is Pro-tier.
146 * - Conversion Tracking / Google Analytics: Pro-only — badge always shown.
147 *
148 * @since 1.8.11.3
149 *
150 * @return string[] key => label.
151 */
152 public function get_sub_sections_marketing() {
153 $pro_badge = ' <span class="charitable-pro-badge">' . esc_html__( 'PRO', 'charitable' ) . '</span>';
154
155 return apply_filters(
156 'charitable_settings_sub_tabs_marketing',
157 array(
158 'marketing__newsletters' => __( 'Newsletters', 'charitable' ) . $pro_badge,
159 'marketing__conversion_tracking' => __( 'Conversion Tracking', 'charitable' ) . $pro_badge,
160 'marketing__google_analytics' => __( 'Google Analytics', 'charitable' ) . $pro_badge,
161 )
162 );
163 }
164
165 /**
166 * Redirect to the default Marketing sub-tab when no sub_tab is supplied.
167 *
168 * @since 1.8.11.3
169 *
170 * @return void
171 */
172 public function marketing_sub_tab_default() {
173 // Don't redirect during form submissions.
174 if ( ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
175 return;
176 }
177
178 if ( ! charitable_is_settings_view( 'marketing' ) ) {
179 return;
180 }
181
182 if ( isset( $_GET['sub_tab'] ) || isset( $_GET['group'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
183 return;
184 }
185
186 wp_safe_redirect(
187 admin_url( 'admin.php?page=charitable-settings&tab=marketing&sub_tab=newsletters' )
188 );
189 exit;
190 }
191
192 /**
193 * Optionally add the extensions tab.
194 *
195 * @since 1.3.0
196 * @version 1.8.5.1 Adjust index so it comes before the advanced tab.
197 *
198 * @param string[] $tabs The existing set of tabs.
199 * @return string[]
200 */
201 public function maybe_add_extensions_tab( $tabs ) {
202 $actual_tab = isset( $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : 'general'; // phpcs:ignore
203
204 /* Set the tab to 'extensions' */
205 $_GET['tab'] = 'extensions';
206
207 /**
208 * Filter the settings in the extensions tab.
209 *
210 * @since 1.3.0
211 *
212 * @param array $fields Array of fields. Empty by default.
213 */
214 $settings = apply_filters( 'charitable_settings_tab_fields_extensions', array() );
215
216 /* Set the tab back to whatever it actually is */
217 $_GET['tab'] = $actual_tab;
218
219 if ( ! empty( $settings ) ) {
220 $tabs = charitable_add_settings_tab(
221 $tabs,
222 'extensions',
223 __( 'Extensions', 'charitable' ),
224 array(
225 'index' => 7,
226 )
227 );
228 }
229
230 // Ensure the advanced tab is always last by moving it to the end of the array.
231 if ( isset( $tabs['advanced'] ) ) {
232 $advanced_tab = $tabs['advanced'];
233 unset( $tabs['advanced'] );
234 $tabs['advanced'] = $advanced_tab;
235 }
236
237 return $tabs;
238 }
239
240 /**
241 * Add the hidden "extensions" section field.
242 *
243 * @since 1.6.7
244 *
245 * @param array $fields All the settings fields.
246 * @return array
247 */
248 public function add_hidden_extensions_setting_field( $fields ) {
249 if ( ! array_key_exists( 'extensions', $fields ) ) {
250 return $fields;
251 }
252
253 $fields['extensions']['section'] = array(
254 'title' => '',
255 'type' => 'hidden',
256 'priority' => 10000,
257 'value' => 'extensions',
258 'save' => false,
259 );
260
261 return $fields;
262 }
263
264 /**
265 * Register setting.
266 *
267 * @since 1.0.0
268 *
269 * @return void
270 */
271 public function register_settings() {
272 if ( ! charitable_is_settings_view() ) {
273 return;
274 }
275
276 register_setting( 'charitable_settings', 'charitable_settings', array( $this, 'sanitize_settings' ) );
277
278 // Bridge the Newsletter Connect addon (which gates its filter on
279 // charitable_is_settings_view('extensions')) so its fields contribute
280 // when the user is on Marketing → Newsletters. We spoof $_GET['tab']
281 // for the duration of get_fields() so the addon's gate passes; the
282 // extensions section is what the view's form group already targets
283 // on the Newsletters sub-tab. The Extensions top-level tab continues
284 // to work as before — this only adds an additional render path.
285 $charitable_bridge_newsletters = (
286 isset( $_GET['tab'] ) && 'marketing' === $_GET['tab'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
287 && isset( $_GET['sub_tab'] ) && 'newsletters' === $_GET['sub_tab'] // phpcs:ignore WordPress.Security.NonceVerification.Recommended
288 && class_exists( 'Charitable_Newsletter_Connect' )
289 );
290
291 if ( $charitable_bridge_newsletters ) {
292 $charitable_real_tab = $_GET['tab']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
293 $_GET['tab'] = 'extensions'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
294 }
295
296 $fields = $this->get_fields();
297
298 if ( $charitable_bridge_newsletters ) {
299 $_GET['tab'] = $charitable_real_tab; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
300 }
301
302 if ( empty( $fields ) ) {
303 return;
304 }
305
306 $sections = array_merge( $this->get_sections(), $this->get_dynamic_groups() );
307
308 /* Register each section */
309 foreach ( $sections as $section_key => $section ) {
310 $section_id = 'charitable_settings_' . $section_key;
311
312 add_settings_section(
313 $section_id,
314 __return_null(),
315 '__return_false',
316 $section_id
317 );
318
319 if ( ! isset( $fields[ $section_key ] ) || empty( $fields[ $section_key ] ) ) {
320 continue;
321 }
322
323 /* Sort by priority */
324 $section_fields = $fields[ $section_key ];
325 uasort( $section_fields, 'charitable_priority_sort' );
326
327 /* Add the individual fields within the section */
328 foreach ( $section_fields as $key => $field ) {
329 $this->register_field( $field, array( $section_key, $key ) );
330 }
331 }
332 }
333
334 /**
335 * Sanitize submitted settings before saving to the database
336 * This includes: "workaround" for Stripe keys with 1.7.0.0, additional settings options after 1.7.0.6+
337 *
338 * @since 1.0.0
339 *
340 * @param array $values The submitted values.
341 * @return string
342 */
343 public function sanitize_settings( $values ) {
344
345 $old_values = get_option( 'charitable_settings', array() );
346 $new_values = array();
347
348 if ( ! is_array( $old_values ) ) {
349 $old_values = array();
350 }
351
352 if ( ! is_array( $values ) ) {
353 $values = array();
354 }
355
356 /* Loop through all fields, merging the submitted values into the master array */
357 foreach ( $values as $section => $submitted ) {
358 $new_values = array_merge( $new_values, $this->get_section_submitted_values( $section, $submitted ) );
359 }
360
361 $settings_keys = array_keys( $new_values );
362
363 if ( charitable_is_debug() ) {
364 error_log( 'santiize_settings' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
365 error_log( 'values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
366 error_log( print_r( $values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
367 error_log( 'old_values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
368 error_log( print_r( $old_values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
369 error_log( 'new_values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
370 error_log( print_r( $new_values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
371 }
372
373 // determine if Charitable gateway "test_mode" is being changed, and if so add a notice to the user.
374 if ( array_key_exists( 'test_mode', $new_values ) && array_key_exists( 'test_mode', $old_values ) && $old_values['test_mode'] !== $new_values['test_mode'] ) {
375 $old_settings_keys = array_keys( $old_values );
376 $dismissible = true;
377
378 // Check if Square has connection issues for the new mode
379 $square_has_issues = $this->check_square_connection_for_mode_change( $new_values['test_mode'] );
380
381 // Determine notice type and message
382 $notice_type = $square_has_issues ? 'error' : 'warning';
383 $gateway_list = array();
384
385 if ( in_array( 'gateways_stripe', $old_settings_keys ) ) {
386 $gateway_list[] = 'Stripe';
387 }
388
389 if ( $square_has_issues ) {
390 $gateway_list[] = 'Square';
391 }
392
393 if ( ! empty( $gateway_list ) ) {
394 $gateway_text = implode( ' and ', $gateway_list );
395 $message = sprintf(
396 /* translators: %s: comma-separated list of gateway names */
397 __( 'Some active payment gateways <strong>(including %s)</strong> might have reset their connections due to an update in the test mode. Please check your active payment gateways to ensure they are still connected.', 'charitable' ),
398 $gateway_text
399 );
400 } else {
401 $message = __( 'Some active payment gateways might have reset their connections due to an update in the test mode. Please check your active payment gateways to ensure they are still connected.', 'charitable' );
402 }
403
404 // Add additional Square-specific message if Square has connection issues
405 if ( $square_has_issues ) {
406 $mode_label = $new_values['test_mode'] ? __( 'test', 'charitable' ) : __( 'live', 'charitable' );
407 $message .= '<br><br><strong>' . sprintf(
408 /* translators: %s: mode (test or live) */
409 __( 'Square payment gateway is enabled but not connected in %s mode. Please connect your Square account to continue processing payments.', 'charitable' ),
410 $mode_label
411 ) . '</strong>';
412 }
413
414 charitable_get_admin_notices()->add_notice( $message, $notice_type, false, $dismissible );
415 }
416
417 if ( in_array( 'gateways_stripe', $settings_keys ) && charitable()->is_stripe_connect_addon() && false === charitable_using_stripe_connect() ) {
418 // non-existant array keys in the array for API keys might be a result of the "manual update key" feature which is based on ajax and might not load keys into form if the user doesn't click on the link in the settings to view/edit them.
419 if ( ! array_key_exists( 'live_secret_key', $values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['live_secret_key'] ) && ( false !== $old_values['gateways_stripe']['live_secret_key'] ) ) {
420 $values['gateways_stripe']['live_secret_key'] = $old_values['gateways_stripe']['live_secret_key'];
421 }
422 if ( ! array_key_exists( 'live_public_key', $values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['live_public_key'] ) && ( false !== $old_values['gateways_stripe']['live_public_key'] ) ) {
423 $values['gateways_stripe']['live_public_key'] = $old_values['gateways_stripe']['live_secret_key'];
424 }
425 if ( ! array_key_exists( 'test_secret_key', $values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['test_secret_key'] ) && ( false !== $old_values['gateways_stripe']['test_secret_key'] ) ) {
426 $values['gateways_stripe']['test_secret_key'] = $old_values['gateways_stripe']['test_secret_key'];
427 }
428 if ( ! array_key_exists( 'test_public_key', $values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['test_public_key'] ) && ( false !== $old_values['gateways_stripe']['test_public_key'] ) ) {
429 $values['gateways_stripe']['test_public_key'] = $old_values['gateways_stripe']['test_public_key'];
430 }
431 if ( charitable_is_debug() ) {
432 error_log( 'update:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
433 error_log( print_r( $values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
434 }
435 }
436
437 // determine if stripe is being returned as new values - if so, then perform the API key save workaround.
438 if ( in_array( 'gateways_stripe', $settings_keys ) && charitable()->is_stripe_connect_addon() && false === charitable_using_stripe_connect() ) {
439 if ( charitable_is_debug() ) {
440 error_log( 'there is a stripe connect addon but we are not using stripe connect' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
441 }
442 // the stripe connect addon is installed AND the AM Stripe Connect isn't being used... so it's possible the keys could be manual and the user could be updating them... therefore let's just make sure the new values match the values incoming.
443 $new_values['gateways_stripe']['live_secret_key'] = ( isset( $values['gateways_stripe']['live_secret_key'] ) && ( false !== $values['gateways_stripe']['live_secret_key'] ) ) ? $values['gateways_stripe']['live_secret_key'] : null;
444 $new_values['gateways_stripe']['live_public_key'] = ( isset( $values['gateways_stripe']['live_public_key'] ) && ( false !== $values['gateways_stripe']['live_public_key'] ) ) ? $values['gateways_stripe']['live_public_key'] : null;
445 $new_values['gateways_stripe']['test_secret_key'] = ( isset( $values['gateways_stripe']['test_secret_key'] ) && ( false !== $values['gateways_stripe']['test_secret_key'] ) ) ? $values['gateways_stripe']['test_secret_key'] : null;
446 $new_values['gateways_stripe']['test_public_key'] = ( isset( $values['gateways_stripe']['test_public_key'] ) && ( false !== $values['gateways_stripe']['test_public_key'] ) ) ? $values['gateways_stripe']['test_public_key'] : null;
447 } elseif ( in_array( 'gateways_stripe', $settings_keys ) ) {
448 // otherwise we preserve the keys.
449 if ( charitable_is_debug() ) {
450 error_log( 'charitable debug, final with debug on:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
451 $new_values['gateways_stripe']['live_secret_key'] = ( isset( $values['gateways_stripe']['live_secret_key'] ) && ( false !== $values['gateways_stripe']['live_secret_key'] ) ) ? $values['gateways_stripe']['live_secret_key'] : null;
452 $new_values['gateways_stripe']['live_public_key'] = ( isset( $values['gateways_stripe']['live_public_key'] ) && ( false !== $values['gateways_stripe']['live_public_key'] ) ) ? $values['gateways_stripe']['live_public_key'] : null;
453 $new_values['gateways_stripe']['test_secret_key'] = ( isset( $values['gateways_stripe']['test_secret_key'] ) && ( false !== $values['gateways_stripe']['test_secret_key'] ) ) ? $values['gateways_stripe']['test_secret_key'] : null;
454 $new_values['gateways_stripe']['test_public_key'] = ( isset( $values['gateways_stripe']['test_public_key'] ) && ( false !== $values['gateways_stripe']['test_public_key'] ) ) ? $values['gateways_stripe']['test_public_key'] : null;
455 } else {
456 $new_values['gateways_stripe']['live_secret_key'] = ( isset( $old_values['gateways_stripe']['live_secret_key'] ) && ( false !== $old_values['gateways_stripe']['live_secret_key'] ) ) ? $old_values['gateways_stripe']['live_secret_key'] : null;
457 $new_values['gateways_stripe']['live_public_key'] = ( isset( $old_values['gateways_stripe']['live_public_key'] ) && ( false !== $old_values['gateways_stripe']['live_public_key'] ) ) ? $old_values['gateways_stripe']['live_public_key'] : null;
458 $new_values['gateways_stripe']['test_secret_key'] = ( isset( $old_values['gateways_stripe']['test_secret_key'] ) && ( false !== $old_values['gateways_stripe']['test_secret_key'] ) ) ? $old_values['gateways_stripe']['test_secret_key'] : null;
459 $new_values['gateways_stripe']['test_public_key'] = ( isset( $old_values['gateways_stripe']['test_public_key'] ) && ( false !== $old_values['gateways_stripe']['test_public_key'] ) ) ? $old_values['gateways_stripe']['test_public_key'] : null;
460 }
461 }
462
463 // preserve any webhook keys, regardless if stripe connect addon is active or not.
464 if ( in_array( 'gateways_stripe', $settings_keys ) ) {
465 if ( ! empty( $old_values['gateways_stripe'] ) && array_key_exists( 'test_webhook_id', $old_values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['test_webhook_id'] ) ) {
466 $new_values['gateways_stripe']['test_webhook_id'] = $old_values['gateways_stripe']['test_webhook_id'];
467 }
468 if ( ! empty( $old_values['gateways_stripe'] ) && array_key_exists( 'live_webhook_id', $old_values['gateways_stripe'] ) && isset( $old_values['gateways_stripe']['live_webhook_id'] ) ) {
469 $new_values['gateways_stripe']['live_webhook_id'] = $old_values['gateways_stripe']['live_webhook_id'];
470 }
471 }
472
473 // account for incoming legacy licenses (1.7.0.4+)
474 // all we are doing is adding the licenses into the values so that the save_license hook can retreieve them and validate, etc.
475 if ( ! empty( $values['legacy_licenses'] ) && isset( $values['advanced']['section_hidden_licenses'] ) && 'licenses' === $values['advanced']['section_hidden_licenses'] ) {
476 if ( ! isset( $new_values['licenses']['legacy'] ) ) {
477 $new_values['licenses_legacy'] = (array) $values['legacy_licenses'];
478 }
479 }
480
481 // ensure any hex color text files are valid hex fields.
482 if ( isset( $new_values['donation_form_default_highlight_colour'] ) ) {
483 $color_to_test = esc_html( $new_values['donation_form_default_highlight_colour'] );
484 $color_valid = false;
485 // check for a hex color string
486 if ( preg_match( '/^#[a-f0-9]{6}$/i', $color_to_test ) ) {
487 // hex color is valid
488 $color_valid = true;
489 }
490 // Check for a hex color string without hash.
491 if ( preg_match( '/^[a-f0-9]{6}$/i', $color_to_test ) ) {
492 // hex color is valid
493 $color_valid = true;
494 $new_values['donation_form_default_highlight_colour'] = '#' . $color_to_test;
495 }
496 if ( false === $color_valid ) {
497 // not valid, make sure it is blank.
498 $new_values['donation_form_default_highlight_colour'] = false;
499 }
500 // clear the transient that stores the color styles.
501 delete_transient( 'charitable_custom_styles' );
502 }
503
504 $values = wp_parse_args( $new_values, $old_values );
505
506 // Sanitize decimal_count to prevent XSS attacks.
507 if ( isset( $values['decimal_count'] ) ) {
508 $values['decimal_count'] = absint( $values['decimal_count'] );
509 // Ensure decimal_count is between 0 and 10.
510 $values['decimal_count'] = min( max( $values['decimal_count'], 0 ), 10 );
511 }
512
513 if ( defined( 'CHARITABLE_DEBUG' ) && CHARITABLE_DEBUG ) {
514 if ( charitable_is_debug() ) {
515 error_log( 'santiize_settings values updated' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
516 error_log( print_r( $values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
517 }
518 }
519
520 /**
521 * Filter sanitized settings.
522 *
523 * @since 1.0.0
524 *
525 * @param array $values All values, merged.
526 * @param array $new_values Newly submitted values.
527 * @param array $old_values Old settings.
528 */
529
530 $values = apply_filters( 'charitable_save_settings', $values, $new_values, $old_values );
531
532 // save a temp transient for legacy licenses (1.7.0.8+)
533 // purpose is to provide better error reporting (transient should be deleted after settings page (advanced tab) is loaded).
534 // phpcs:disable WordPress.Security.NonceVerification.Missing
535 if ( isset( $_POST['charitable_settings']['legacy_licenses'] ) ) {
536 $referer = wp_get_referer();
537 if ( false !== strpos( $referer, 'admin.php?page=charitable-settings&tab=advanced' ) ) {
538 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Array data, sanitized later
539 $legacy_licenses = wp_unslash( $_POST['charitable_settings']['legacy_licenses'] );
540 set_transient( '_charitable_legacy_license_info', $legacy_licenses, MINUTE_IN_SECONDS );
541 }
542 }
543 // phpcs:enable WordPress.Security.NonceVerification.Missing
544
545 $this->add_update_message( __( 'Settings saved', 'charitable' ), 'success' );
546
547 return $values;
548 }
549
550 /**
551 * Checkbox settings should always be either 1 or 0.
552 *
553 * @since 1.0.0
554 *
555 * @param mixed $value Submitted value for field.
556 * @param array $field Field definition.
557 * @return int
558 */
559 public function sanitize_checkbox_value( $value, $field ) {
560 if ( isset( $field['type'] ) && 'checkbox' == $field['type'] ) {
561 $value = intval( $value && 'on' == $value );
562 }
563
564 return $value;
565 }
566
567 /**
568 * Render field. This is the default callback used for all fields, unless an alternative callback has been specified.
569 *
570 * @since 1.0.0
571 *
572 * @param array $args Field definition.
573 * @return void
574 */
575 public function render_field( $args ) {
576 $field_type = isset( $args['type'] ) ? $args['type'] : 'text';
577
578 charitable_admin_view( 'settings/' . $field_type, $args );
579 }
580
581 /**
582 * Returns an array of all pages in the id=>title format.
583 *
584 * @since 1.0.0
585 *
586 * @return string[]
587 */
588 public function get_pages() {
589 if ( ! isset( $this->pages ) ) {
590 $this->pages = charitable_get_pages_options();
591 }
592
593 return $this->pages;
594 }
595
596 /**
597 * Add an update message.
598 *
599 * @since 1.4.6
600 *
601 * @param string $message The message text.
602 * @param string $type The type of message. Options: 'error', 'success', 'warning', 'info'.
603 * @param boolean $dismissible Whether the message can be dismissed.
604 * @return void
605 */
606 public function add_update_message( $message, $type = 'error', $dismissible = true ) {
607 if ( ! in_array( $type, array( 'error', 'success', 'warning', 'info' ) ) ) {
608 $type = 'error';
609 }
610
611 charitable_get_admin_notices()->add_notice( $message, $type, false, $dismissible );
612 }
613
614 /**
615 * Recursively add settings fields, given an array.
616 *
617 * @since 1.0.0
618 * @version 1.8.7
619 *
620 * @param array $field The setting field.
621 * @param array $keys Array containing the section key and field key.
622 * @return void
623 */
624 private function register_field( $field, $keys ) {
625 $section_id = 'charitable_settings_' . $keys[0];
626
627 if ( isset( $field['render'] ) && ! $field['render'] ) {
628 return;
629 }
630
631 /* Drop the first key, which is the section identifier */
632 $field['name'] = implode( '][', $keys );
633
634 if ( ! $this->is_dynamic_group( $keys[0] ) ) {
635 array_shift( $keys );
636 }
637
638 $field['key'] = $keys;
639 $field['classes'] = $this->get_field_classes( $field ); // deprecated?
640 $field['class'] = $this->get_field_classes( $field );
641 $callback = isset( $field['callback'] ) ? $field['callback'] : array( $this, 'render_field' );
642 $label = $this->get_field_label( $field, end( $keys ) );
643
644 add_settings_field(
645 sprintf( 'charitable_settings_%s', implode( '_', $keys ) ),
646 $label,
647 $callback,
648 $section_id,
649 $section_id,
650 $field
651 );
652 }
653
654 /**
655 * Return the label for the given field.
656 *
657 * @since 1.0.0
658 *
659 * @param array $field The field definition.
660 * @param string $key The field key.
661 * @return string
662 */
663 private function get_field_label( $field, $key ) { // phpcs:ignore
664 $label = '';
665
666 if ( isset( $field['label_for'] ) ) {
667 $label = $field['label_for'];
668 }
669
670 if ( isset( $field['title'] ) ) {
671 $label = $field['title'];
672 }
673
674 return $label;
675 }
676
677 /**
678 * Return a space separated string of classes for the given field.
679 *
680 * @since 1.0.0
681 *
682 * @param array $field Field definition.
683 * @return string
684 */
685 private function get_field_classes( $field ) {
686 $classes = array( 'charitable-settings-field' );
687
688 if ( isset( $field['class'] ) ) {
689 $classes[] = $field['class'];
690 }
691
692 /**
693 * Filter the list of classes to apply to settings fields.
694 *
695 * @since 1.0.0
696 *
697 * @param array $classes The list of classes.
698 * @param array $field The field definition.
699 */
700 $classes = apply_filters( 'charitable_settings_field_classes', $classes, $field );
701
702 return implode( ' ', $classes );
703 }
704
705 /**
706 * Return an array with all the fields & sections to be displayed.
707 *
708 * @uses charitable_settings_fields
709 * @see Charitable_Settings::register_setting()
710 * @since 1.0.0
711 *
712 * @return array
713 */
714 private function get_fields() {
715 /**
716 * Use the charitable_settings_tab_fields to include the fields for new tabs.
717 * DO NOT use it to add individual fields. That should be done with the
718 * filters within each of the methods.
719 */
720 $fields = array();
721
722 foreach ( $this->get_sections() as $section_key => $section ) {
723 /**
724 * Filter the array of fields to display in a particular tab.
725 *
726 * @since 1.0.0
727 *
728 * @param array $fields Array of fields.
729 */
730 $fields[ $section_key ] = apply_filters( 'charitable_settings_tab_fields_' . $section_key, array() );
731 }
732
733 /**
734 * Filter the array of settings fields.
735 *
736 * @since 1.0.0
737 *
738 * @param array $fields Array of fields.
739 */
740 return apply_filters( 'charitable_settings_tab_fields', $fields );
741 }
742
743 /**
744 * Get the submitted value for a particular setting.
745 *
746 * @since 1.0.0
747 *
748 * @param string $key The key of the setting being saved.
749 * @param array $field The setting field.
750 * @param array $submitted The submitted values.
751 * @param string $section The section being saved.
752 * @return mixed|null Returns null if the value was not submitted or is not applicable.
753 */
754 private function get_setting_submitted_value( $key, $field, $submitted, $section ) {
755 $value = null;
756
757 if ( isset( $field['save'] ) && ! $field['save'] ) {
758 return $value;
759 }
760
761 $field_type = isset( $field['type'] ) ? $field['type'] : '';
762
763 switch ( $field_type ) {
764
765 case 'checkbox':
766 $value = intval( array_key_exists( $key, $submitted ) && ( 'on' === $submitted[ $key ] || 1 === (int) $submitted[ $key ] ) );
767 break;
768
769 case 'multi-checkbox':
770 $value = isset( $submitted[ $key ] ) ? $submitted[ $key ] : array();
771 break;
772
773 case '':
774 case 'heading':
775 return $value;
776
777 default:
778 if ( ! array_key_exists( $key, $submitted ) ) {
779 return $value;
780 }
781
782 $value = $submitted[ $key ];
783
784 }//end switch
785
786 /**
787 * General way to sanitize values. If you only need to sanitize a
788 * specific setting, used the filter below instead.
789 *
790 * @since 1.0.0
791 *
792 * @param mixed $value The current setting value.
793 * @param array $field The field configuration.
794 * @param array $submitted All submitted data.
795 * @param string $key The setting key.
796 * @param string $section The section being saved.
797 */
798 $value = apply_filters( 'charitable_sanitize_value', $value, $field, $submitted, $key, $section );
799
800 /**
801 * Sanitize the setting value.
802 *
803 * The filter hook is formatted like this: charitable_sanitize_value_{$section}_{$key}.
804 *
805 * @since 1.5.0
806 *
807 * @param mixed $value The current setting value.
808 * @param array $field The field configuration.
809 * @param array $submitted All submitted data.
810 */
811 return apply_filters( 'charitable_sanitize_value_' . $section . '_' . $key, $value, $field, $submitted );
812 }
813
814 /**
815 * Return the submitted values for the given section.
816 *
817 * @since 1.0.0
818 *
819 * @param string $section The section being edited.
820 * @param array $submitted The submitted values.
821 * @return array
822 */
823 private function get_section_submitted_values( $section, $submitted ) {
824 $values = array();
825 $form_fields = $this->get_fields();
826
827 if ( ! isset( $form_fields[ $section ] ) ) {
828 return $values;
829 }
830
831 foreach ( $form_fields[ $section ] as $key => $field ) {
832 $value = $this->get_setting_submitted_value( $key, $field, $submitted, $section );
833
834 if ( is_null( $value ) ) {
835 continue;
836 }
837
838 if ( $this->is_dynamic_group( $section ) ) {
839 $values[ $section ][ $key ] = $value;
840 continue;
841 }
842
843 $values[ $key ] = $value;
844 }
845
846 return $values;
847 }
848
849 /**
850 * Return list of dynamic groups.
851 *
852 * @since 1.0.0
853 *
854 * @return string[]
855 */
856 private function get_dynamic_groups() {
857 if ( ! isset( $this->dynamic_groups ) ) {
858 /**
859 * Filter the list of dynamic groups.
860 *
861 * @since 1.0.0
862 *
863 * @param array $groups The dynamic groups.
864 */
865 $this->dynamic_groups = apply_filters( 'charitable_dynamic_groups', array() );
866 }
867
868 return $this->dynamic_groups;
869 }
870
871 /**
872 * Returns whether the given key indicates the start of a new section of the settings.
873 *
874 * @since 1.0.0
875 *
876 * @param string $composite_key The unique key for this group.
877 * @return boolean
878 */
879 private function is_dynamic_group( $composite_key ) {
880 return array_key_exists( $composite_key, $this->get_dynamic_groups() );
881 }
882
883 /* DEPRECATED FUNCTIONS */
884
885 /**
886 * Get the update messages.
887 *
888 * @deprecated 1.7.0
889 *
890 * @since 1.4.13 Deprecated.
891 */
892 public function get_update_messages() {
893 charitable_get_deprecated()->deprecated_function(
894 __METHOD__,
895 '1.4.13',
896 'Charitable_Admin_Notices::get_notices()'
897 );
898
899 return charitable_get_admin_notices()->get_notices();
900 }
901
902 /**
903 * Display upgrade notice at the bottom on the plugin settings pages.
904 *
905 * @since 1.7.0.4
906 * @version 1.8.8
907 *
908 * @param string $view Current view inside the plugin settings page.
909 * @param string $css_class CSS class for the cta.
910 * @param bool $show_close_button Whether to show the close button.
911 */
912 public function settings_cta( $view = false, $css_class = 'reports-lite-cta', $show_close_button = false ) { // phpcs:ignore
913
914 $no_show_cta = array(
915 'donors',
916 'security',
917 'marketing',
918 );
919
920 if ( in_array( $view, $no_show_cta ) ) {
921 return;
922 }
923
924 if ( charitable_is_pro() ) {
925 // no need to display this cta since they have a valid license.
926 return;
927 }
928
929 if ( get_option( 'charitable_lite_settings_upgrade', false ) || apply_filters( 'charitable_lite_settings_upgrade', false ) ) {
930 return;
931 }
932 ?>
933 <?php if ( $show_close_button ) : ?>
934 <div class="charitable-cta-dismiss-container">
935 <button type="button" class="button-link charitable-banner-dismiss dismiss">x</button>
936 </div>
937 <?php endif; ?>
938 <?php charitable_render_global_upgrade_cta( $css_class ); ?>
939 <script type="text/javascript">
940 jQuery( function ( $ ) {
941 $( document ).on( 'click', '.settings-lite-cta .dismiss', function ( event ) {
942 event.preventDefault();
943 $.post( ajaxurl, {
944 action: 'charitable_lite_settings_upgrade',
945 charitable_action: 'remove_lite_cta'
946 } );
947 $( '.settings-lite-cta' ).remove();
948 } );
949 } );
950 </script>
951 <?php
952 }
953
954 /**
955 * Show the settings CTA.
956 *
957 * @since 1.8.5.1
958 * @version 1.8.9
959 *
960 * @return void
961 */
962 public function show_settings_cta() {
963
964 $tab_values_to_check = array(
965 'marketing',
966 'donors',
967 'security',
968 );
969
970 $tab_value = isset( $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : ''; // phpcs:ignore
971 $sub_tab_value = isset( $_GET['sub_tab'] ) ? esc_attr( $_GET['sub_tab'] ) : ''; // phpcs:ignore
972
973 if ( in_array( $tab_value, $tab_values_to_check ) ) { // phpcs:ignore
974 // UTM medium follows the sub_tab when present (e.g. marketing-conversion-tracking)
975 // so each upgrade button is attributable.
976 $upgrade_medium = ( 'marketing' === $tab_value && ! empty( $sub_tab_value ) )
977 ? 'marketing-' . str_replace( '_', '-', $sub_tab_value )
978 : $tab_value;
979
980 ob_start();
981 ?>
982
983 <div class="charitable-education-page">
984
985 <?php
986 if ( 'marketing' === $tab_value ) {
987 // Default to the Newsletters CTA when no sub_tab is supplied
988 // (the marketing_sub_tab_default redirect should normally prevent this,
989 // but render safely if a redirect was skipped — e.g. during POST).
990 $marketing_sub_tab = '' !== $sub_tab_value ? $sub_tab_value : 'newsletters';
991
992 if ( 'conversion_tracking' === $marketing_sub_tab ) {
993 $this->get_settings_cta_education_content_conversion_tracking();
994 } elseif ( 'google_analytics' === $marketing_sub_tab ) {
995 $this->get_settings_cta_education_content_google_analytics();
996 } else {
997 $this->get_settings_cta_education_content_marketing_settings();
998 }
999 } elseif ( 'donors' === $tab_value ) {
1000 $this->get_settings_cta_education_content_donor_settings();
1001 } elseif ( 'security' === $tab_value && ! charitable_is_pro() ) {
1002 $this->get_settings_cta_education_content_advanced_security_settings();
1003 }
1004 ?>
1005 <?php if ( charitable_is_pro() ) : ?>
1006 <div class="charitable-education-page-disabled-download">
1007 <p>
1008 <?php
1009 printf(
1010 /* translators: %s: Documentation URL */
1011 esc_html__( 'Please add your license and upgrade to Charitable Pro for more advanced security features. Please %s on how to download Charitable Pro.', 'charitable' ),
1012 '<a href="https://wpcharitable.com/docs/download-charitable-pro/" target="_blank">' . esc_html__( 'see our documentation', 'charitable' ) . '</a>'
1013 );
1014 ?>
1015 </p>
1016 </div>
1017 <?php else : ?>
1018 <div class="charitable-education-page-button">
1019 <a href="<?php echo esc_url( charitable_utm_link( 'https://wpcharitable.com/lite-upgrade/', $upgrade_medium, __( 'Upgrade to Charitable Pro', 'charitable' ) ) ); ?>" class="button button-primary" target="_blank"><?php esc_html_e( 'Upgrade to Charitable Pro', 'charitable' ); ?></a>
1020 </div>
1021 <?php endif; ?>
1022 </div>
1023 <?php
1024 echo ob_get_clean(); // phpcs:ignore
1025 }
1026 }
1027
1028 /**
1029 * Get the settings CTA education content for the donor referrals sub tab.
1030 *
1031 * @since 1.8.5.1
1032 *
1033 * @return void
1034 */
1035 public function get_settings_cta_education_content_donor_settings() {
1036 ?>
1037
1038 <div class="charitable-education-page-heading">
1039 <h4><?php esc_html_e( 'New Donor Features', 'charitable' ); ?></h4>
1040 <p><?php esc_html_e( 'Give donors the power to manage their contributions to your organization with a new donor dashboard. Encourage donors to spread the word about your organization by offering rewards for successful referrals. And give donors the ability to post comments to campaigns when they donate.', 'charitable' ); ?></p>
1041 <p><?php esc_html_e( 'All of this is possible with the Charitable Pro plugin.', 'charitable' ); ?></p>
1042 </div>
1043
1044 <div class="charitable-education-page-media">
1045 <div class="charitable-education-page-images">
1046 <figure>
1047 <div class="charitable-education-page-images-image">
1048 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-dashboard/education-1.png" alt="<?php esc_attr_e( 'Donor Dashboard', 'charitable' ); ?>">
1049 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-dashboard/education-1.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Donor Dashboard', 'charitable' ); ?>"></a>
1050 </div>
1051 <figcaption><?php esc_html_e( 'Donor Dashboard', 'charitable' ); ?></figcaption>
1052 </figure>
1053 <figure>
1054 <div class="charitable-education-page-images-image">
1055 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-referrals/education-2.png" alt="<?php esc_attr_e( 'Donor Referrals', 'charitable' ); ?>">
1056 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-referrals/education-2.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Donor Referrals', 'charitable' ); ?>"></a>
1057 </div>
1058 <figcaption><?php esc_html_e( 'Donor Referrals', 'charitable' ); ?></figcaption>
1059 </figure>
1060 <figure>
1061 <div class="charitable-education-page-images-image">
1062 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-comments/education-3.png" alt="<?php esc_attr_e( 'Donor Comments', 'charitable' ); ?>">
1063 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/donor-comments/education-3.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Donor Comments', 'charitable' ); ?>"></a>
1064 </div>
1065 <figcaption><?php esc_html_e( 'Donor Comments', 'charitable' ); ?></figcaption>
1066 </figure>
1067 </div>
1068 </div>
1069
1070 <div class="charitable-education-page-caps">
1071 <p><?php esc_html_e( 'Give donors the power to manage their contributions to your organization.', 'charitable' ); ?></p>
1072 <ul>
1073 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Increased Donor Engagement', 'charitable' ); ?></li>
1074 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Customizable Dashboard', 'charitable' ); ?></li>
1075 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Donation History', 'charitable' ); ?></li>
1076 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'View Campaigns', 'charitable' ); ?></li>
1077 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'View Comments', 'charitable' ); ?></li>
1078 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Download Donation Receipts', 'charitable' ); ?></li>
1079 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Tracking Personalized Notifications', 'charitable' ); ?></li>
1080 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Donor Profile Management', 'charitable' ); ?></li>
1081 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'See Each Donor\'s Referrals', 'charitable' ); ?></li>
1082 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Track Referral Success Rates', 'charitable' ); ?></li>
1083 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Customizable Referral Rewards', 'charitable' ); ?></li>
1084 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Easy Promotion of Campaigns', 'charitable' ); ?></li>
1085
1086 </ul>
1087 </div>
1088 <?php
1089 }
1090
1091 /**
1092 * Get the settings CTA education content for the donor referrals sub tab.
1093 *
1094 * @since 1.8.5.1
1095 *
1096 * @return void
1097 */
1098 public function get_settings_cta_education_content_marketing_settings() {
1099 ?>
1100
1101 <div class="charitable-education-page-heading">
1102 <h4><?php esc_html_e( 'Marketing', 'charitable' ); ?></h4>
1103 <p><?php esc_html_e( 'Turn one-time donors into ongoing supporters with your newsletter list. This feature allows you to connect to your email marketing account and automatically add donors to your email newsletter list. You can integrate your campaigns seamlessly with MailChimp, Campaign Monitor, MailerLite, ActiveCampaign, Mailster and MailPoet, allowing you to enable one or more providers.', 'charitable' ); ?></p>
1104 </div>
1105
1106 <div class="charitable-education-page-media">
1107 <div class="charitable-education-page-images">
1108 <figure>
1109 <div class="charitable-education-page-images-image">
1110 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-1.png" alt="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>">
1111 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-1.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>"></a>
1112 </div>
1113 <figcaption><?php esc_html_e( 'Choose a default list', 'charitable' ); ?></figcaption>
1114 </figure>
1115 <figure>
1116 <div class="charitable-education-page-images-image">
1117 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-2.png" alt="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>">
1118 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-2.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>"></a>
1119 </div>
1120 <figcaption><?php esc_html_e( 'Sync lists', 'charitable' ); ?></figcaption>
1121 </figure>
1122 <figure>
1123 <div class="charitable-education-page-images-image">
1124 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-3.png" alt="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>">
1125 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/marketing/education-3.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Marketing', 'charitable' ); ?>"></a>
1126 </div>
1127 <figcaption><?php esc_html_e( 'Donation form options', 'charitable' ); ?></figcaption>
1128 </figure>
1129 </div>
1130 </div>
1131
1132 <div class="charitable-education-page-caps">
1133 <p><?php esc_html_e( 'Turn one-time donors into ongoing supporters.', 'charitable' ); ?></p>
1134 <ul>
1135 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Easy Integration', 'charitable' ); ?></li>
1136 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Built-in contact consent options', 'charitable' ); ?></li>
1137 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Automatically add donors to your email list', 'charitable' ); ?></li>
1138 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Organize donors with tags', 'charitable' ); ?></li>
1139 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Send email series to keep donors engaged', 'charitable' ); ?></li>
1140 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Enable multiple email services', 'charitable' ); ?></li>
1141 </ul>
1142 </div>
1143 <?php
1144 }
1145
1146 /**
1147 * Get the settings CTA education content for the Marketing → Conversion Tracking sub-tab.
1148 *
1149 * @since 1.8.11.3
1150 *
1151 * @return void
1152 */
1153 public function get_settings_cta_education_content_conversion_tracking() {
1154 $assets_url = charitable()->get_path( 'assets', false );
1155 ?>
1156
1157 <div class="charitable-education-page-heading">
1158 <h4><?php esc_html_e( 'Conversion Tracking', 'charitable' ); ?></h4>
1159 <p><?php esc_html_e( 'Send Charitable donation events into Meta Pixel, TikTok Pixel, and Google Tag Manager. Native, code-free conversion tracking that ties every completed donation to the ad campaigns and audiences that drove it.', 'charitable' ); ?></p>
1160 <p><?php esc_html_e( 'See which ads actually produce donations, optimize spend toward real revenue, and build remarketing audiences from your highest-value supporters.', 'charitable' ); ?></p>
1161 </div>
1162
1163 <div class="charitable-education-page-media">
1164 <div class="charitable-education-page-video">
1165 <figure>
1166 <div class="charitable-education-page-video-embed">
1167 <iframe src="https://www.youtube-nocookie.com/embed/sS-F6x6F-Fs?rel=0&amp;modestbranding=1&amp;iv_load_policy=3&amp;loop=1&amp;playlist=sS-F6x6F-Fs" title="<?php esc_attr_e( 'Introducing Conversion Tracking', 'charitable' ); ?>" allow="accelerometer; autoplay; clipboard-write; encrypted-media; picture-in-picture" allowfullscreen frameborder="0"></iframe>
1168 </div>
1169 <figcaption><a href="https://youtu.be/sS-F6x6F-Fs" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Watch: Introducing Conversion Tracking', 'charitable' ); ?></a></figcaption>
1170 </figure>
1171 </div>
1172 <div class="charitable-education-page-images">
1173 <figure>
1174 <div class="charitable-education-page-images-image">
1175 <img src="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-1.png" alt="<?php esc_attr_e( 'Conversion tracking settings', 'charitable' ); ?>">
1176 <a href="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-1.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Pixel + GTM settings', 'charitable' ); ?>"></a>
1177 </div>
1178 <figcaption><?php esc_html_e( 'Pixel + GTM settings', 'charitable' ); ?></figcaption>
1179 </figure>
1180 <figure>
1181 <div class="charitable-education-page-images-image">
1182 <img src="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-2.png" alt="<?php esc_attr_e( 'Per-donation tracking status', 'charitable' ); ?>">
1183 <a href="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-2.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Per-donation tracking status', 'charitable' ); ?>"></a>
1184 </div>
1185 <figcaption><?php esc_html_e( 'Per-donation tracking status', 'charitable' ); ?></figcaption>
1186 </figure>
1187 <figure>
1188 <div class="charitable-education-page-images-image">
1189 <img src="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-3.png" alt="<?php esc_attr_e( 'WP Consent API compliant', 'charitable' ); ?>">
1190 <a href="<?php echo esc_url( $assets_url ); ?>images/education/conversion-tracking/education-3.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'WP Consent API compliant', 'charitable' ); ?>"></a>
1191 </div>
1192 <figcaption><?php esc_html_e( 'WP Consent API compliant', 'charitable' ); ?></figcaption>
1193 </figure>
1194 </div>
1195 </div>
1196
1197 <div class="charitable-education-page-caps">
1198 <p><?php esc_html_e( 'Track conversions with…', 'charitable' ); ?></p>
1199 <ul>
1200 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Meta Pixel (Facebook / Instagram)', 'charitable' ); ?></li>
1201 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'TikTok Pixel', 'charitable' ); ?></li>
1202 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Google Tag Manager (GTM)', 'charitable' ); ?></li>
1203 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Campaign page view tracking', 'charitable' ); ?></li>
1204 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Donation form open tracking', 'charitable' ); ?></li>
1205 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'WP Consent API (GDPR) integration', 'charitable' ); ?></li>
1206 </ul>
1207 </div>
1208 <?php
1209 }
1210
1211 /**
1212 * Get the settings CTA education content for the Marketing → Google Analytics sub-tab.
1213 *
1214 * @since 1.8.11.3
1215 *
1216 * @return void
1217 */
1218 public function get_settings_cta_education_content_google_analytics() {
1219 $assets_url = charitable()->get_path( 'assets', false );
1220 ?>
1221
1222 <div class="charitable-education-page-heading">
1223 <h4><?php esc_html_e( 'Google Analytics', 'charitable' ); ?></h4>
1224 <p><?php esc_html_e( 'Send Charitable donation events into Google Analytics 4. Native GA4 integration fires purchase, refund, sign-up, and login events both browser-side via gtag.js and server-side via the Measurement Protocol — so every donation, including off-site gateways and webhook-confirmed completions, makes it into GA4.', 'charitable' ); ?></p>
1225 <p><?php esc_html_e( 'See which campaigns and sources actually produce donations, stitch sessions across devices with hashed-email user_id, and stay EEA-compliant out of the box via Google Consent Mode v2 and the WP Consent API.', 'charitable' ); ?></p>
1226 </div>
1227
1228 <div class="charitable-education-page-media">
1229 <div class="charitable-education-page-images">
1230 <figure>
1231 <div class="charitable-education-page-images-image">
1232 <img src="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-1.png" alt="<?php esc_attr_e( 'Google Analytics settings', 'charitable' ); ?>">
1233 <a href="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-1.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'GA4 settings + Test Connection', 'charitable' ); ?>"></a>
1234 </div>
1235 <figcaption><?php esc_html_e( 'GA4 settings + Test Connection', 'charitable' ); ?></figcaption>
1236 </figure>
1237 <figure>
1238 <div class="charitable-education-page-images-image">
1239 <img src="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-2.png" alt="<?php esc_attr_e( 'Per-donation GA metabox', 'charitable' ); ?>">
1240 <a href="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-2.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Per-donation tracking metabox', 'charitable' ); ?>"></a>
1241 </div>
1242 <figcaption><?php esc_html_e( 'Per-donation tracking metabox', 'charitable' ); ?></figcaption>
1243 </figure>
1244 <figure>
1245 <div class="charitable-education-page-images-image">
1246 <img src="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-3.png" alt="<?php esc_attr_e( 'Test connection success', 'charitable' ); ?>">
1247 <a href="<?php echo esc_url( $assets_url ); ?>images/education/google-analytics/education-3.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Validate your GA4 setup', 'charitable' ); ?>"></a>
1248 </div>
1249 <figcaption><?php esc_html_e( 'Validate your GA4 setup', 'charitable' ); ?></figcaption>
1250 </figure>
1251 </div>
1252 </div>
1253
1254 <div class="charitable-education-page-caps">
1255 <p><?php esc_html_e( 'Track donations in GA4 with…', 'charitable' ); ?></p>
1256 <ul>
1257 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'GA4 purchase events (browser + Measurement Protocol)', 'charitable' ); ?></li>
1258 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Server-side refund, sign_up, and login events', 'charitable' ); ?></li>
1259 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Full UTM capture (including utm_term and utm_id)', 'charitable' ); ?></li>
1260 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Hashed-email user_id for cross-device session stitching', 'charitable' ); ?></li>
1261 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Google Consent Mode v2 + WP Consent API integration', 'charitable' ); ?></li>
1262 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( '"Test Connection" button to validate your GA4 setup', 'charitable' ); ?></li>
1263 </ul>
1264 </div>
1265 <?php
1266 }
1267
1268 /**
1269 * Get the settings CTA education content for the donor referrals sub tab.
1270 *
1271 * @since 1.8.5.1
1272 *
1273 * @return void
1274 */
1275 public function get_settings_cta_education_content_security_settings() {
1276 ?>
1277
1278 <div class="charitable-education-page-heading">
1279 <h4><?php esc_html_e( 'Security', 'charitable' ); ?></h4>
1280 <p><?php esc_html_e( 'It is important to protect against potential misuse while maintaining donor trust. CAPTCHA integration adds an essential layer of protection against automated attacks and spam donations. Every transaction is meticulously logged with IP address documentation and timestamp verification, providing a clear audit trail and helping prevent fraudulent activities.', 'charitable' ); ?></p>
1281 </div>
1282
1283 <div class="charitable-education-page-media">
1284 <div class="charitable-education-page-images">
1285 <figure>
1286 <div class="charitable-education-page-images-image">
1287 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/security/education-1.png" alt="<?php esc_attr_e( 'Security', 'charitable' ); ?>">
1288 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/security/education-1.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Security', 'charitable' ); ?>"></a>
1289 </div>
1290 <figcaption><?php esc_html_e( 'Captchas', 'charitable' ); ?></figcaption>
1291 </figure>
1292 <figure>
1293 <div class="charitable-education-page-images-image">
1294 <img src="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/security/education-3.png" alt="<?php esc_attr_e( 'Security', 'charitable' ); ?>">
1295 <a href="<?php echo esc_url( charitable()->get_path( 'assets', false ) ); ?>images/education/security/education-3.png" class="hover" data-lity="" data-lity-desc="<?php esc_attr_e( 'Security', 'charitable' ); ?>"></a>
1296 </div>
1297 <figcaption><?php esc_html_e( 'Form options', 'charitable' ); ?></figcaption>
1298 </figure>
1299 </div>
1300 </div>
1301
1302 <div class="charitable-education-page-caps">
1303 <p><?php esc_html_e( 'Protect against potential misuse while maintaining donor trust.', 'charitable' ); ?></p>
1304 <ul>
1305 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Easy Activation', 'charitable' ); ?></li>
1306 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Record IP Addresses', 'charitable' ); ?></li>
1307 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Block Bad Bots', 'charitable' ); ?></li>
1308 </ul>
1309 </div>
1310 <?php
1311 }
1312
1313 /**
1314 * Get the settings CTA education content for advanced security features.
1315 *
1316 * @since 1.8.9
1317 *
1318 * @return void
1319 */
1320 public function get_settings_cta_education_content_advanced_security_settings() {
1321 ?>
1322
1323 <div class="charitable-education-page-heading">
1324 <h4><?php esc_html_e( 'Advanced Security Features', 'charitable' ); ?></h4>
1325 <p><?php esc_html_e( 'Charitable Pro offers a fortified layer of safety designed for high-volume campaigns. Upgrade to unlock sophisticated spam filtering and enhanced payment protection, ensuring the highest level of trust and security for your donors.', 'charitable' ); ?></p>
1326 </div>
1327 <div class="charitable-education-page-caps">
1328 <p><?php esc_html_e( 'Protect against potential misuse while maintaining donor trust.', 'charitable' ); ?></p>
1329 <ul>
1330 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'General Rate Limiting', 'charitable' ); ?></li>
1331 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Stripe Rate Limiting', 'charitable' ); ?></li>
1332 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'IP Blacklist/Whitelist', 'charitable' ); ?></li>
1333 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Email Pattern Blocking', 'charitable' ); ?></li>
1334 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Minimum Donation Features', 'charitable' ); ?></li>
1335 <li><i class="fa fa-solid fa-check"></i> <?php esc_html_e( 'Math Field Protection', 'charitable' ); ?></li>
1336 </ul>
1337 </div>
1338 <?php
1339 }
1340
1341 /**
1342 * Dismiss upgrade notice at the bottom on the plugin settings pages.
1343 *
1344 * @since 1.7.0.4
1345 *
1346 * @return void
1347 */
1348 public function settings_cta_dismiss() {
1349
1350 if ( ! charitable_current_user_can() ) {
1351 wp_send_json_error();
1352 }
1353
1354 update_option( 'charitable_lite_settings_upgrade', time() );
1355
1356 wp_send_json_success();
1357 }
1358
1359 /**
1360 * Check Square connection for the new mode.
1361 *
1362 * @since 1.8.5
1363 *
1364 * @param string $new_mode The new test mode.
1365 * @return boolean True if Square has connection issues, false otherwise.
1366 */
1367 private function check_square_connection_for_mode_change( $new_mode ) {
1368 // Check if Square gateway is active.
1369 $active_gateways = charitable_get_helper( 'gateways' )->get_active_gateways();
1370
1371 if ( ! is_array( $active_gateways ) || ! in_array( 'Charitable_Gateway_Square', $active_gateways ) ) {
1372 return false;
1373 }
1374
1375 // Determine the mode string.
1376 $mode = $new_mode ? 'test' : 'live';
1377
1378 // Check if there's a Square connection for this mode.
1379 $connection = Charitable_Square_Connection::get( $mode );
1380
1381 // Check if connection exists and is properly configured.
1382 if ( ! $connection || ! $connection->is_configured() || ! $connection->is_valid() ) {
1383
1384 // Set a warning flag that will be checked on the payment page.
1385 update_option( 'charitable_square_mode_connection_warning', array(
1386 'mode' => $mode,
1387 'timestamp' => time(),
1388 ) );
1389
1390 return true; // Square has connection issues.
1391 } else {
1392 // Clear the warning if connection exists.
1393 delete_option( 'charitable_square_mode_connection_warning' );
1394 return false; // Square has no connection issues.
1395 }
1396 }
1397 }
1398
1399 endif;
1400