PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.10
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.10
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 1.7.0.9 1.8.0 All 210 releases
charitable / charitable.php

charitable.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.10, at charitable.php

1,465 lines 43.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plugin Name: Charitable
4 * Plugin URI: https://www.wpcharitable.com
5 * Description: The best WordPress donation plugin. Fundraising with recurring donations, and powerful features to help you raise more money online.
6 * Version: 1.8.10
7 * Author: Charitable Donations & Fundraising Team
8 * Author URI: https://wpcharitable.com
9 * Requires at least: 5.0
10 * Stable tag: 1.8.10
11 * License: GPLv2 or later
12 * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
13 *
14 * Text Domain: charitable
15 * Domain Path: /i18n/languages/
16 *
17 * @package Charitable
18 * @author David Bisset
19 * @copyright Copyright (c) 2026, WP Charitable LLC
20 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
21 */
22
23 // Exit if accessed directly.
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 if ( ! class_exists( 'Charitable' ) ) :
29
30 /**
31 * Main Charitable class.
32 */
33 class Charitable {
34
35 /** The product name. */
36 const NAME = 'Charitable';
37
38 /** The plugin author name. */
39 const AUTHOR = 'WP Charitable';
40
41 /* Plugin version. */
42 const VERSION = '1.8.10';
43
44 /* Version of database schema. */
45 const DB_VERSION = '20180522';
46
47 /* Campaign post type. */
48 const CAMPAIGN_POST_TYPE = 'campaign';
49
50 /* Donation post type. */
51 const DONATION_POST_TYPE = 'donation';
52
53 /**
54 * Single instance of this class.
55 *
56 * @since 1.0.0
57 *
58 * @var Charitable
59 */
60 private static $instance = null;
61
62 /**
63 * The absolute path to this plugin's directory.
64 *
65 * @since 1.0.0
66 *
67 * @var string
68 */
69 private $directory_path;
70
71 /**
72 * The URL of this plugin's directory.
73 *
74 * @since 1.0.0
75 *
76 * @var string
77 */
78 private $directory_url;
79
80 /**
81 * Directory path for the includes folder of the plugin.
82 *
83 * @since 1.0.0
84 *
85 * @var string
86 */
87 private $includes_path;
88
89 /**
90 * Store of registered objects.
91 *
92 * @since 1.0.0
93 * @since 1.5.0 Changed to Charitable_Registry object. Previously it was an array.
94 *
95 * @var Charitable_Registry
96 */
97 private $registry;
98
99 /**
100 * Classmap.
101 *
102 * @since 1.5.0
103 *
104 * @var array
105 */
106 private $classmap;
107
108 /**
109 * Plugin.
110 *
111 * @since 1.8.0
112 *
113 * @var array
114 */
115 private $plugin;
116
117 /**
118 * Create class instance.
119 *
120 * @since 1.0.0
121 */
122 public function __construct() {
123 $this->directory_path = plugin_dir_path( __FILE__ );
124 $this->directory_url = plugin_dir_url( __FILE__ );
125 $this->includes_path = $this->directory_path . 'includes/';
126
127 define( 'CHARITABLE_DIRECTORY_PATH', plugin_dir_path( __FILE__ ) );
128 define( 'CHARITABLE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
129 define( 'CHARITABLE_MARKETING_URL', 'https://wpcharitable.com/' );
130
131 define( 'CHARITABLE_BUILDER_SHOW_LEGACY_EDIT_LINKS', true ); // 1.8.0 specific.
132
133 spl_autoload_register( array( $this, 'autoloader' ) );
134
135 $this->load_dependencies();
136
137 register_activation_hook( __FILE__, array( $this, 'activate' ) );
138 register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
139
140 add_filter( 'plugin_action_links_' . CHARITABLE_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ), 99, 2 );
141 add_action( 'plugins_loaded', array( $this, 'start' ), 3 );
142
143 add_action( 'plugins_loaded', array( $this, 'check_for_version_conflicts' ), 2 );
144
145 // Run upgrade-server callback before any redirect (e.g. login) can run. Request from connect-callback.php.
146 if ( defined( 'CHARITABLE_CONNECT_CALLBACK' ) && CHARITABLE_CONNECT_CALLBACK ) {
147 add_action( 'init', array( $this, 'run_connect_callback' ), -9999 );
148 }
149
150 // Initialize error handler early.
151 add_action(
152 'plugins_loaded',
153 function () {
154 require_once $this->get_path( 'includes' ) . 'class-charitable-error-handler.php';
155 $error_handler = new Charitable_Error_Handler();
156 $error_handler->init();
157 },
158 1
159 );
160 }
161
162 /**
163 * Returns the original instance of this class.
164 *
165 * @since 1.0.0
166 *
167 * @return Charitable
168 */
169 public static function get_instance() {
170 return self::$instance;
171 }
172
173 /**
174 * Run the startup sequence.
175 *
176 * This is only ever executed once.
177 *
178 * @since 1.0.0
179 *
180 * @return void
181 */
182 public function start() {
183 /* If we've already started (i.e. run this function once before), do not pass go. */
184 if ( $this->started() ) {
185 return;
186 }
187
188 /* Set static instance. */
189 self::$instance = $this;
190
191 /* Set up the registry and instantiate objects we need straight away. */
192 $this->registry();
193
194 $this->setup_licensing();
195
196 $this->maybe_start_ajax();
197
198 $this->attach_hooks_and_filters();
199
200 $this->maybe_start_admin();
201
202 $this->maybe_start_public();
203
204 Charitable_Addons::load( $this );
205 }
206
207 /**
208 * Include necessary files.
209 *
210 * @since 1.0.0
211 *
212 * @return void
213 */
214 private function load_dependencies() {
215 $includes_path = $this->get_path( 'includes' );
216
217 /* Load files with hooks & functions. Classes are autoloaded. */
218 require_once $includes_path . 'charitable-core-functions.php';
219 require_once $includes_path . 'api/charitable-api-functions.php';
220 require_once $includes_path . 'campaigns/charitable-campaign-functions.php';
221 require_once $includes_path . 'campaigns/charitable-campaign-hooks.php';
222 require_once $includes_path . 'compat/charitable-compat-functions.php';
223 require_once $includes_path . 'currency/charitable-currency-functions.php';
224 require_once $includes_path . 'deprecated/charitable-deprecated-functions.php';
225 require_once $includes_path . 'donations/charitable-donation-hooks.php';
226 require_once $includes_path . 'donations/charitable-donation-functions.php';
227 require_once $includes_path . 'emails/charitable-email-hooks.php';
228 require_once $includes_path . 'endpoints/charitable-endpoints-functions.php';
229 require_once $includes_path . 'privacy/charitable-privacy-functions.php';
230 require_once $includes_path . 'public/charitable-template-helpers.php';
231 require_once $includes_path . 'shortcodes/charitable-shortcodes-hooks.php';
232 require_once $includes_path . 'upgrades/charitable-upgrade-hooks.php';
233 require_once $includes_path . 'users/charitable-user-functions.php';
234 require_once $includes_path . 'user-management/charitable-user-management-hooks.php';
235 require_once $includes_path . 'utilities/charitable-utility-functions.php';
236 require_once $includes_path . 'elementor/charitable-elementor-hooks.php';
237
238 // Load security hooks on both admin and frontend (CAPTCHA needs to work on frontend).
239 require_once $this->get_path( 'includes', true ) . 'admin/security/charitable-security-hooks.php';
240
241 /* Load vendor/autoload.php - our modified platform_check.php and autoload_real.php will handle conditional loading */
242 require_once $this->get_path( 'directory' ) . 'vendor/autoload.php';
243
244 /* Square Compatibility Wrapper - always load this regardless of PHP version */
245 require_once $includes_path . 'square/class-charitable-square-compatibility.php';
246
247 /* Initialize Square conditionally - will check for compatibility internally */
248 Charitable_Square_Compatibility::init();
249
250 if ( $this->load_core_stripe() ) :
251
252 /* Interfaces */
253 require_once $includes_path . 'stripe/interfaces/interface-charitable-stripe-gateway-processor.php';
254
255 /* Abstract classes */
256 require_once $includes_path . 'stripe/abstracts/class-charitable-stripe-gateway-processor.php';
257
258 /* Classes */
259 require_once $includes_path . 'stripe/donations/class-charitable-stripe-donation-log.php';
260 require_once $includes_path . 'stripe/donations/class-charitable-stripe-recurring-donation-log.php';
261 require_once $includes_path . 'stripe/i18n/class-charitable-stripe-i18n.php';
262 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-connected-customer.php';
263 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-customer.php';
264 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-gateway-processor-payment-intents.php';
265 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-payment-intent.php';
266 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-plan.php';
267 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-product.php';
268
269 require_once $includes_path . 'stripe/connect/charitable-stripe-connect-functions.php';
270 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-gateway-processor-checkout.php';
271
272 /* Functions & Hooks */
273 require_once $includes_path . 'stripe/charitable-stripe-core-functions.php';
274 require_once $includes_path . 'stripe/donations/charitable-stripe-donation-functions.php';
275 require_once $includes_path . 'stripe/gateway/charitable-stripe-gateway-hooks.php';
276
277 /* webhooks */
278 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-webhook-api.php';
279 require_once $includes_path . 'stripe/gateway/class-charitable-stripe-webhook-processor.php';
280
281 endif;
282 }
283
284 /**
285 * Load the template functions after theme is loaded.
286 *
287 * This gives themes time to override the functions.
288 *
289 * @since 1.6.10
290 *
291 * @return void
292 */
293 public function load_template_files() {
294 $includes_path = $this->get_path( 'includes' );
295
296 require_once $includes_path . 'public/charitable-template-functions.php';
297 require_once $includes_path . 'public/charitable-template-hooks.php';
298 }
299
300 /**
301 * Dynamically loads the class attempting to be instantiated elsewhere in the
302 * plugin by looking at the $class_name parameter being passed as an argument.
303 *
304 * @since 1.5.0
305 *
306 * @param string $class_name The fully-qualified name of the class to load.
307 * @return boolean
308 */
309 public function autoloader( $class_name ) {
310 /* If the specified $class_name already exists, bail. */
311 if ( class_exists( $class_name ) ) {
312 return false;
313 }
314
315 /* If the specified $class_name does not include our namespace, duck out. */
316 if ( false === strpos( $class_name, 'Charitable_' ) ) {
317 return false;
318 }
319
320 /* Autogenerated class map. */
321 if ( ! isset( $this->classmap ) ) {
322 $this->classmap = include 'includes/autoloader/charitable-class-map.php';
323
324 $this->classmap['Charitable_Gateway_Stripe_AM'] = 'gateways/class-charitable-gateway-stripe-am.php';
325 $this->classmap['Charitable_Gateway_Stripe'] = 'gateways/class-charitable-gateway-stripe-am.php';
326 }
327
328 $file_path = isset( $this->classmap[ $class_name ] ) ? $this->get_path( 'includes' ) . $this->classmap[ $class_name ] : false;
329
330 if ( false !== $file_path && file_exists( $file_path ) && is_file( $file_path ) ) {
331 require_once $file_path;
332 return true;
333 }
334 return false;
335 }
336
337 /**
338 * Returns a registered class object.
339 *
340 * @since 1.5.0
341 *
342 * @return Charitable_Registry
343 */
344 public function registry() {
345 if ( ! isset( $this->registry ) ) {
346 $this->registry = new Charitable_Registry();
347 $this->registry->register_object( Charitable_Emails::get_instance() );
348 $this->registry->register_object( Charitable_Request::get_instance() );
349 $this->registry->register_object( Charitable_Gateways::get_instance() );
350 $this->registry->register_object( Charitable_i18n::get_instance() );
351 $this->registry->register_object( Charitable_Post_Types::get_instance() );
352 $this->registry->register_object( Charitable_Cron::get_instance() );
353 $this->registry->register_object( Charitable_Widgets::get_instance() );
354 $this->registry->register_object( Charitable_Licenses::get_instance() );
355 $this->registry->register_object( Charitable_User_Dashboard::get_instance() );
356 $this->registry->register_object( Charitable_Locations::get_instance() );
357 $this->registry->register_object( Charitable_Currency::get_instance() );
358 $this->registry->register_object( Charitable_Notifications::get_instance() );
359
360 $this->registry->register_object( new Charitable_Privacy() );
361 $this->registry->register_object( new Charitable_Debugging() );
362 $this->registry->register_object( new Charitable_Locale() );
363 }
364
365 return $this->registry;
366 }
367
368 /**
369 * Set up hook and filter callback functions.
370 *
371 * @since 1.0.0
372 *
373 * @return void
374 */
375 private function attach_hooks_and_filters() {
376 add_action( 'wpmu_new_blog', array( $this, 'maybe_activate_charitable_on_new_site' ) );
377 add_action( 'plugins_loaded', array( $this, 'charitable_install' ), 100 );
378 add_action( 'plugins_loaded', array( $this, 'charitable_start' ), 100 );
379 add_action( 'plugins_loaded', array( $this, 'endpoints' ), 100 );
380 add_action( 'init', array( $this, 'donation_fields' ), 100 );
381 add_action( 'init', array( $this, 'campaign_fields' ), 100 );
382 add_action( 'plugins_loaded', array( $this, 'register_donormeta_table' ) );
383 add_action( 'plugins_loaded', 'charitable_load_compat_functions' );
384 add_action( 'setup_theme', array( 'Charitable_Customizer', 'start' ) );
385 add_action( 'after_setup_theme', array( $this, 'load_template_files' ) );
386 add_action( 'wp_enqueue_scripts', array( $this, 'maybe_start_qunit' ), 100 );
387 add_action( 'rest_api_init', 'charitable_register_api_routes' );
388
389 /**
390 * We do this on priority 20 so that any functionality that is loaded on init (such
391 * as addons) has a chance to run before the event.
392 */
393 add_action( 'init', array( $this, 'do_charitable_actions' ), 20 );
394
395 if ( $this->load_core_stripe() ) :
396
397 /**
398 * Set up scripts & stylesheets & enqueue them when appropriate.
399 */
400 add_action( 'charitable_form_after_fields', array( $this, 'maybe_setup_stripe_scripts_in_donation_form' ) );
401 add_action( 'charitable_campaign_loop_after', array( $this, 'maybe_setup_stripe_scripts_in_campaign_loop' ) );
402
403 endif;
404 }
405
406
407 /**
408 * Load Stripe JS, as well as our handling scripts.
409 *
410 * @since 1.4.0
411 *
412 * @return boolean
413 */
414 public function enqueue_stripe_scripts() {
415 if ( ! Charitable_Gateways::get_instance()->is_active_gateway( Charitable_Gateway_Stripe_AM::get_gateway_id() ) ) {
416 return false;
417 }
418
419 wp_enqueue_script( 'charitable-stripe' );
420
421 return true;
422 }
423
424 /**
425 * Load Stripe JS or Stripe Checkout, as well as our handling script.
426 *
427 * @uses Charitable_Stripe::enqueue_scripts()
428 *
429 * @since 1.4.0
430 *
431 * @param Charitable_Donation_Form $form The current form object.
432 * @return boolean
433 */
434 public function maybe_setup_stripe_scripts_in_donation_form( $form ) {
435 if ( ! is_a( $form, 'Charitable_Donation_Form' ) ) {
436 return false;
437 }
438
439 if ( 'make_donation' !== $form->get_form_action() ) {
440 return false;
441 }
442
443 return $this->enqueue_stripe_scripts();
444 }
445
446 /**
447 * Enqueue the Stripe JS/Checkout scripts after a campaign loop if modal donations are in use.
448 *
449 * @uses Charitable_Stripe::enqueue_scripts()
450 *
451 * @since 1.4.0
452 *
453 * @return boolean
454 */
455 public function maybe_setup_stripe_scripts_in_campaign_loop() {
456 if ( 'modal' !== charitable_get_option( 'donation_form_display', 'separate_page' ) ) {
457 return false;
458 }
459
460 return $this->enqueue_stripe_scripts();
461 }
462
463 /**
464 * Checks whether we're in the admin area and if so, loads the admin-only functionality.
465 *
466 * @since 1.0.0
467 *
468 * @return Charitable_Admin|false
469 */
470 private function maybe_start_admin() {
471 if ( ! is_admin() ) {
472 return false;
473 }
474
475 // load blocks/Gutenberg related.
476 require_once $this->get_path( 'admin' ) . 'class-charitable-admin-blocks.php';
477
478 require_once $this->get_path( 'admin' ) . 'class-charitable-admin.php';
479 require_once $this->get_path( 'admin' ) . 'charitable-admin-hooks.php';
480
481 // load campaign builder.
482 require_once $this->get_path( 'admin' ) . 'campaign-builder/util.php';
483 require_once $this->get_path( 'admin' ) . 'campaign-builder/functions.php';
484 require_once $this->get_path( 'admin' ) . 'campaign-builder/ajax-actions.php';
485 require_once $this->get_path( 'admin' ) . 'campaign-builder/functions-legacy.php';
486 require_once $this->get_path( 'admin' ) . 'campaign-builder/debug.php';
487 require_once $this->get_path( 'admin' ) . 'campaign-builder/access.php';
488 require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-fields.php';
489 require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-form-fields.php';
490
491 // connect for upgrade.wpcharitable.com.
492 require_once $this->get_path( 'admin' ) . 'class-charitable-admin-connect.php';
493
494 // misc.
495 require_once $this->get_path( 'admin' ) . 'class-charitable-admin-pointers.php';
496 require_once $this->get_path( 'admin' ) . 'plugins/charitable-admin-plugin-hooks.php';
497 require_once $this->get_path( 'admin' ) . 'class-charitable-admin-getting-started.php';
498
499 $admin = Charitable_Admin::get_instance();
500 $this->registry->register_object( $admin );
501
502 // Stripe.
503 require_once $this->get_path( 'includes' ) . 'stripe/admin/class-charitable-stripe-admin.php';
504 new Charitable_Stripe_Admin();
505
506 return $admin;
507 }
508
509 /**
510 * Run the upgrade-server connect callback on init before any redirect (e.g. to login) can run.
511 * Only registered when CHARITABLE_CONNECT_CALLBACK is defined (request came from connect-callback.php).
512 *
513 * @since 1.8.9.6
514 */
515 public function run_connect_callback() {
516 ini_set( 'display_errors', '0' );
517 require_once ABSPATH . 'wp-admin/includes/plugin.php';
518 require_once ABSPATH . 'wp-admin/includes/file.php';
519 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
520 $connect_file = $this->get_path( 'admin' ) . 'class-charitable-admin-connect.php';
521 if ( file_exists( $connect_file ) ) {
522 require_once $connect_file;
523 }
524 if ( ! class_exists( 'Charitable_Admin_Connect' ) ) {
525 header( 'Content-Type: application/json' );
526 wp_send_json( array( 'success' => false, 'data' => array( 'message' => 'Charitable not loaded.' ) ) );
527 }
528 Charitable_Admin_Connect::get_instance()->process();
529 exit;
530 }
531
532 /**
533 * Checks whether we're on the public-facing side and if so, loads the public-facing functionality.
534 *
535 * @since 1.0.0
536 *
537 * @return Charitable_Public|false
538 */
539 private function maybe_start_public() {
540 if ( is_admin() && ! $this->is_ajax() ) {
541 return false;
542 }
543
544 require_once $this->get_path( 'public' ) . 'class-charitable-public.php';
545
546 // Campaign Builder related (v1.8.0).
547 require_once $this->get_path( 'admin' ) . 'campaign-builder/class-campaign-builder-preview.php';
548 require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-fields.php';
549 require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-form-fields.php';
550
551 // load blocks/Gutenberg related.
552 require_once $this->get_path( 'admin' ) . 'class-charitable-admin-blocks.php';
553
554 $public = Charitable_Public::get_instance();
555
556 $this->registry->register_object( $public );
557
558 return $public;
559 }
560
561 /**
562 * Load the QUnit tests if ?qunit is appended to the request.
563 *
564 * @since 1.4.17
565 *
566 * @return boolean
567 */
568 public function maybe_start_qunit() {
569 /* Skip out early if ?qunit isn't included in the request. */
570 if ( ! array_key_exists( 'qunit', $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
571 return false;
572 }
573
574 /* The unit tests have to exist. */
575 if ( ! file_exists( $this->get_path( 'directory' ) . 'tests/qunit/tests.js' ) ) {
576 return false;
577 }
578
579 /**
580 * Register QUnit testing library.
581 *
582 * Note: These files must be bundled locally for WordPress.org compliance.
583 * Download from:
584 * - JS: https://code.jquery.com/qunit/qunit-2.3.3.js
585 * - CSS: https://code.jquery.com/qunit/qunit-2.3.3.css
586 * Save to:
587 * - assets/js/libraries/qunit-2.3.3.js
588 * - assets/css/libraries/qunit-2.3.3.css
589 *
590 * @since 1.0.0
591 * @version 1.8.9.1
592 */
593 wp_register_script(
594 'qunit',
595 $this->get_path( 'directory', false ) . 'assets/js/libraries/qunit-2.3.3.js',
596 array(),
597 '2.3.3',
598 true
599 );
600
601 /* Version: '20170615-15:44' */
602 wp_register_script( 'qunit-tests', $this->get_path( 'directory', false ) . 'tests/qunit/tests.js', array( 'jquery', 'qunit' ), time(), true );
603 wp_enqueue_script( 'qunit-tests' );
604
605 wp_register_style(
606 'qunit',
607 $this->get_path( 'directory', false ) . 'assets/css/libraries/qunit-2.3.3.css',
608 array(),
609 '2.3.3',
610 'all'
611 );
612 wp_enqueue_style( 'qunit' );
613 }
614
615 /**
616 * Checks whether the current request is an AJAX request.
617 *
618 * @since 1.5.0
619 *
620 * @return boolean
621 */
622 private function is_ajax() {
623 return false !== ( defined( 'DOING_AJAX' ) && DOING_AJAX );
624 }
625
626 /**
627 * Checks whether we're executing an AJAX hook and if so, loads some AJAX functionality.
628 *
629 * @since 1.0.0
630 *
631 * @return void
632 */
633 private function maybe_start_ajax() {
634 if ( ! $this->is_ajax() ) {
635 return;
636 }
637
638 require_once $this->get_path( 'includes' ) . 'ajax/charitable-ajax-functions.php';
639 require_once $this->get_path( 'includes' ) . 'ajax/charitable-ajax-hooks.php';
640 }
641
642 /**
643 * This method is fired after all plugins are loaded and simply fires the charitable_start hook.
644 *
645 * Extensions can use the charitable_start event to load their own functionality.
646 *
647 * @since 1.0.0
648 *
649 * @return void
650 */
651 public function charitable_start() {
652 do_action( 'charitable_start', $this );
653 }
654
655 /**
656 * Fires off an action right after Charitable is installed, allowing other
657 * plugins/themes to do something at this point.
658 *
659 * @since 1.0.1
660 * @version 1.8.1.12 - Added transient for activation redirect.
661 * @version 1.8.3 - Added call to add version and delete notifications.
662 *
663 * @return void
664 */
665 public function charitable_install() {
666 $install = get_transient( 'charitable_install' );
667
668 if ( ! $install ) {
669 return;
670 }
671
672 require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-install.php';
673
674 Charitable_Install::finish_installing();
675
676 do_action( 'charitable_install' );
677
678 delete_transient( 'charitable_install' );
679
680 // Store the date when the initial activation was performed.
681 $type = function_exists( 'charitable_is_pro' ) && charitable_is_pro() ? 'pro' : 'lite';
682 $activated = (array) get_option( 'charitable_activated', array() );
683
684 // If no activation date is set, assume it's a new install and set the activation redirect transient.
685 if ( false === $activated || empty( $activated ) ) {
686 // Add transient to trigger redirect to the Welcome / Getting Started screen.
687 set_transient( 'charitable_activation_redirect', true, 30 );
688 }
689
690 $this->add_version_to_upgraded_from();
691
692 if ( empty( $activated[ $type ] ) ) {
693 $activated[ $type ] = time();
694 update_option( 'charitable_activated', $activated );
695 }
696
697 // Clear notifications.
698 delete_option( 'charitable_notifications' );
699 }
700
701 /**
702 * Returns whether we are currently in the start phase of the plugin.
703 *
704 * @since 1.0.0
705 *
706 * @return boolean
707 */
708 public function is_start() {
709 return current_filter() == 'charitable_start'; // phpcs:ignore
710 }
711
712 /**
713 * Returns whether the plugin has already started.
714 *
715 * @since 1.0.0
716 *
717 * @return boolean
718 */
719 public function started() {
720 return did_action( 'charitable_start' ) || current_filter() == 'charitable_start'; // phpcs:ignore
721 }
722
723 /**
724 * Returns whether the plugin is being activated.
725 *
726 * @since 1.0.0
727 *
728 * @return boolean
729 */
730 public function is_activation() {
731 return current_filter() == 'activate_charitable/charitable.php'; // phpcs:ignore
732 }
733
734 /**
735 * Returns whether the plugin is being deactivated.
736 *
737 * @since 1.0.0
738 *
739 * @return boolean
740 */
741 public function is_deactivation() {
742 return current_filter() == 'deactivate_charitable/charitable.php'; // phpcs:ignore
743 }
744
745 /**
746 * Returns plugin paths.
747 *
748 * @since 1.0.0
749 *
750 * @param string $type If empty, returns the path to the plugin.
751 * @param boolean $absolute_path If true, returns the file system path. If false, returns it as a URL.
752 * @return string
753 */
754 public function get_path( $type = '', $absolute_path = true ) {
755 $base = $absolute_path ? $this->directory_path : $this->directory_url;
756
757 switch ( $type ) {
758 case 'includes':
759 $path = $base . 'includes/';
760 break;
761
762 case 'admin':
763 $path = $base . 'includes/admin/';
764 break;
765
766 case 'public':
767 $path = $base . 'includes/public/';
768 break;
769
770 case 'assets':
771 $path = $base . 'assets/';
772 break;
773
774 case 'templates':
775 $path = $base . 'templates/';
776 break;
777
778 case 'plugin-directory':
779 $path = WP_PLUGIN_DIR;
780 break;
781
782 case 'directory':
783 $path = $base;
784 break;
785
786 default:
787 $path = __FILE__;
788
789 }//end switch
790
791 return $path;
792 }
793
794 /**
795 * Returns the plugin's version number.
796 *
797 * @since 1.0.0
798 *
799 * @return string
800 */
801 public function get_version() {
802 $version = self::VERSION;
803
804 if ( false !== strpos( $version, '-' ) ) {
805 $parts = explode( '-', $version );
806 $version = $parts[0];
807 }
808
809 return $version;
810 }
811
812 /**
813 * Adds previous versions to the upgraded_from option.
814 *
815 * @since 1.8.3
816 */
817 public function add_version_to_upgraded_from() {
818
819 $upgraded_from = get_option( 'charitable_version_upgraded_from', array() );
820
821 if ( ! is_array( $upgraded_from ) ) {
822 $upgraded_from = array();
823 }
824
825 $upgraded_from[] = $this->get_version();
826
827 update_option( 'charitable_version_upgraded_from', $upgraded_from, false );
828 }
829
830 /**
831 * Get the campaign fields registry
832 *
833 * If the registry has not been set up yet, this will also
834 * perform the task of setting it up initially.
835 *
836 * This method is called on the plugins_loaded hook.
837 *
838 * @since 1.6.0
839 *
840 * @return Charitable_Campaign_Field_Registry
841 */
842 public function campaign_fields() {
843 if ( ! $this->registry->has( 'campaign_field_registry' ) ) {
844 $campaign_fields = new Charitable_Campaign_Field_Registry();
845
846 /* Register it immediately to avoid endless recursion. */
847 $this->registry->register_object( $campaign_fields );
848
849 $fields = include $this->get_path( 'includes' ) . 'fields/default-fields/campaign-fields.php';
850
851 foreach ( $fields as $key => $args ) {
852 $campaign_fields->register_field( new Charitable_Campaign_Field( $key, $args ) );
853 }
854 }
855
856 return $this->registry->get( 'campaign_field_registry' );
857 }
858
859 /**
860 * Get the donation fields registry
861 *
862 * If the registry has not been set up yet, this will also
863 * perform the task of setting it up initially.
864 *
865 * This method is called on the plugins_loaded hook.
866 *
867 * @since 1.5.0
868 *
869 * @return Charitable_Donation_Field_Registry
870 */
871 public function donation_fields() {
872 if ( ! $this->registry->has( 'donation_field_registry' ) ) {
873 $donation_fields = new Charitable_Donation_Field_Registry();
874
875 /* Register it immediately to avoid endless recursion. */
876 $this->registry->register_object( $donation_fields );
877
878 $fields = include $this->get_path( 'includes' ) . 'fields/default-fields/donation-fields.php';
879
880 foreach ( $fields as $key => $args ) {
881 $donation_fields->register_field( new Charitable_Donation_Field( $key, $args ) );
882 }
883 }
884
885 return $this->registry->get( 'donation_field_registry' );
886 }
887
888 /**
889 * Return the Endpoints API object.
890 *
891 * If the Endpoints API has not been set up yet, this will also
892 * perform the task of setting it up initially.
893 *
894 * This method is called on the plugins_loaded hook.
895 *
896 * @since 1.5.0
897 *
898 * @return Charitable_Endpoints
899 */
900 public function endpoints() {
901 if ( ! $this->registry->has( 'endpoints' ) ) {
902 /**
903 * The order in which we register endpoints is important, because
904 * it determines the order in which the endpoints are checked to
905 * find whether they are the current page.
906 *
907 * Any endpoint that builds on another endpoint should be registered
908 * BEFORE the endpoint it builds on. In other words, move from
909 * most specific to least specific.
910 */
911 $endpoints = new Charitable_Endpoints();
912 $endpoints->register( new Charitable_Campaign_Donation_Endpoint() );
913 $endpoints->register( new Charitable_Campaign_Widget_Endpoint() );
914 $endpoints->register( new Charitable_Campaign_Endpoint() );
915 $endpoints->register( new Charitable_Donation_Cancellation_Endpoint() );
916 $endpoints->register( new Charitable_Donation_Processing_Endpoint() );
917 $endpoints->register( new Charitable_Donation_Receipt_Endpoint() );
918 $endpoints->register( new Charitable_Email_Preview_Endpoint() );
919 $endpoints->register( new Charitable_Email_Verification_Endpoint() );
920 $endpoints->register( new Charitable_Forgot_Password_Endpoint() );
921 $endpoints->register( new Charitable_Reset_Password_Endpoint() );
922 $endpoints->register( new Charitable_Registration_Endpoint() );
923 $endpoints->register( new Charitable_Login_Endpoint() );
924 $endpoints->register( new Charitable_Profile_Endpoint() );
925 $endpoints->register( new Charitable_Webhook_Listener_Endpoint() );
926
927 $this->registry->register_object( $endpoints );
928 }
929
930 return $this->registry->get( 'endpoints' );
931 }
932
933 /**
934 * Returns a registered object.
935 *
936 * @since 1.0.0
937 *
938 * @param string $class The type of class to be retrieved.
939 * @return object
940 */
941 public function get_registered_object( $class ) { // phpcs:ignore
942 return $this->registry->get( $class );
943 }
944
945 /**
946 * Registers our donormeta table as a meta data table.
947 *
948 * @since 1.6.0
949 *
950 * @global WPDB $wpdb
951 * @return void
952 */
953 public function register_donormeta_table() {
954 global $wpdb;
955
956 $wpdb->donormeta = $wpdb->prefix . 'charitable_donormeta';
957 }
958
959 /**
960 * Returns the model for one of Charitable's database tables.
961 *
962 * @since 1.0.0
963 *
964 * @param string $table The database table to retrieve.
965 * @return Charitable_DB
966 */
967 public function get_db_table( $table ) {
968 $tables = $this->get_tables();
969
970 if ( ! isset( $tables[ $table ] ) ) {
971 charitable_get_deprecated()->doing_it_wrong(
972 __METHOD__,
973 sprintf( 'Invalid table %s passed', $table ),
974 '1.0.0'
975 );
976 return null;
977 }
978
979 return $this->registry->get( $tables[ $table ] );
980 }
981
982 /**
983 * Return the filtered list of registered tables.
984 *
985 * @since 1.0.0
986 *
987 * @return string[]
988 */
989 private function get_tables() {
990 /**
991 * Filter the array of available Charitable table classes.
992 *
993 * @since 1.0.0
994 * @version 1.8.1
995 *
996 * @param array $tables List of tables as a key=>value array.
997 */
998 return apply_filters(
999 'charitable_db_tables',
1000 array(
1001 'campaign_donations' => 'Charitable_Campaign_Donations_DB',
1002 'donors' => 'Charitable_Donors_DB',
1003 'donormeta' => 'Charitable_Donormeta_DB',
1004 'donation_activities' => 'Charitable_Donation_Activities_DB',
1005 'campaign_activities' => 'Charitable_Campaign_Activities_DB',
1006 )
1007 );
1008 }
1009
1010 /**
1011 * Maybe activate Charitable when a new site is added in a multisite network.
1012 *
1013 * @since 1.4.6
1014 *
1015 * @param int $blog_id The blog to activate Charitable on.
1016 * @return void
1017 */
1018 public function maybe_activate_charitable_on_new_site( $blog_id ) {
1019 if ( is_plugin_active_for_network( basename( $this->directory_path ) . '/charitable.php' ) ) {
1020 switch_to_blog( $blog_id );
1021 $this->activate( false );
1022 restore_current_blog();
1023 }
1024 }
1025
1026 /**
1027 * Runs on plugin activation.
1028 *
1029 * @see register_activation_hook
1030 *
1031 * @since 1.0.0
1032 *
1033 * @param boolean $network_wide Whether to enable the plugin for all sites in the network
1034 * or just the current site. Multisite only. Default is false.
1035 * @return void
1036 */
1037 public function activate( $network_wide = false ) {
1038 require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-install.php';
1039
1040 if ( is_multisite() && $network_wide ) {
1041 global $wpdb;
1042
1043 foreach ( $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" ) as $blog_id ) { // phpcs:ignore
1044 switch_to_blog( $blog_id );
1045 new Charitable_Install( $this->includes_path );
1046 restore_current_blog();
1047 }
1048 } else {
1049 new Charitable_Install( $this->includes_path );
1050 }
1051
1052 // create or update the install date for future reference.
1053 update_option( 'wpcharitable_activated_datetime', current_time( 'timestamp' ) ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
1054
1055 // Set initial timestamps for rotating menu items if they're already installed
1056 $this->set_initial_rotating_menu_timestamps();
1057 }
1058
1059 /**
1060 * Runs on plugin deactivation.
1061 *
1062 * @see register_deactivation_hook
1063 *
1064 * @since 1.0.0
1065 *
1066 * @return void
1067 */
1068 public function deactivate() {
1069 require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-uninstall.php';
1070 new Charitable_Uninstall();
1071 }
1072
1073 /**
1074 * If a charitable_action event is triggered, delegate the event using do_action.
1075 *
1076 * @since 1.0.0
1077 *
1078 * @return void
1079 */
1080 public function do_charitable_actions() {
1081 if ( isset( $_REQUEST['charitable_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1082
1083 $action = esc_attr( $_REQUEST['charitable_action'] ); // phpcs:ignore
1084
1085 /**
1086 * Handle Charitable action.
1087 *
1088 * @since 1.0.0
1089 */
1090 do_action( 'charitable_' . $action );
1091 }
1092 }
1093
1094 /**
1095 * Throw error on object clone.
1096 *
1097 * This class is specifically designed to be instantiated once. You can retrieve the instance using charitable()
1098 *
1099 * @since 1.0.0
1100 *
1101 * @return void
1102 */
1103 public function __clone() {
1104 charitable_get_deprecated()->doing_it_wrong(
1105 __FUNCTION__,
1106 __( 'Cloning this object is forbidden.', 'charitable' ),
1107 '1.0.0'
1108 );
1109 }
1110
1111 /**
1112 * Disable unserializing of the class.
1113 *
1114 * @since 1.0.0
1115 *
1116 * @return void
1117 */
1118 public function __wakeup() {
1119 charitable_get_deprecated()->doing_it_wrong(
1120 __FUNCTION__,
1121 __( 'Unserializing instances of this class is forbidden.', 'charitable' ),
1122 '1.0.0'
1123 );
1124 }
1125
1126 /**
1127 * DEPRECATED METHODS
1128 */
1129
1130 /**
1131 * Load plugin compatibility files on plugins_loaded hook.
1132 *
1133 * @deprecated 1.8.0
1134 *
1135 * @since 1.4.18
1136 * @since 1.5.0 Deprecated.
1137 *
1138 * @return void
1139 */
1140 public function load_plugin_compat_files() {
1141 charitable_get_deprecated()->deprecated_function(
1142 __METHOD__,
1143 '1.5.0.',
1144 'charitable_load_compat_functions'
1145 );
1146
1147 charitable_load_compat_functions();
1148 }
1149
1150 /**
1151 * Stores an object in the plugin's registry.
1152 *
1153 * @deprecated 1.8.0
1154 *
1155 * @since 1.0.0
1156 * @since 1.5.0 Deprecated.
1157 *
1158 * @param mixed $object Object to be registered.
1159 * @return void
1160 */
1161 public function register_object( $object ) { // phpcs:ignore
1162 charitable_get_deprecated()->deprecated_function(
1163 __METHOD__,
1164 '1.5.0',
1165 'charitable()->registry()->register_object()'
1166 );
1167
1168 $this->registry->register_object( $object );
1169 }
1170
1171 /**
1172 * Returns the public class.
1173 *
1174 * @deprecated 1.8.0
1175 *
1176 * @since 1.0.0
1177 * @since 1.5.0 Deprecated.
1178 *
1179 * @return Charitable_Public
1180 */
1181 public function get_public() {
1182 charitable_get_deprecated()->deprecated_function(
1183 __METHOD__,
1184 '1.5.0',
1185 'charitable_get_helper'
1186 );
1187
1188 return $this->registry->get( 'Charitable_Public' );
1189 }
1190
1191 /**
1192 * Returns the admin class.
1193 *
1194 * @deprecated 1.8.0
1195 *
1196 * @since 1.0.0
1197 * @since 1.5.0 Deprecated.
1198 *
1199 * @return Charitable_Admin
1200 */
1201 public function get_admin() {
1202 charitable_get_deprecated()->deprecated_function(
1203 __METHOD__,
1204 '1.5.0',
1205 'charitable_get_helper'
1206 );
1207
1208 return $this->registry->get( 'Charitable_Admin' );
1209 }
1210
1211 /**
1212 * Return the current request object.
1213 *
1214 * @deprecated 1.8.0
1215 *
1216 * @since 1.0.0
1217 * @since 1.5.0 Deprecated.
1218 *
1219 * @return Charitable_Request
1220 */
1221 public function get_request() {
1222 charitable_get_deprecated()->deprecated_function(
1223 __METHOD__,
1224 '1.5.0',
1225 'charitable_get_helper'
1226 );
1227
1228 return $this->registry->get( 'Charitable_Request' );
1229 }
1230
1231 /**
1232 * Returns the instance of Charitable_Currency.
1233 *
1234 * @deprecated 1.7.0
1235 *
1236 * @since 1.4.0 Deprecated.
1237 */
1238 public function get_currency_helper() {
1239 charitable_get_deprecated()->deprecated_function( __METHOD__, '1.4.0', 'charitable_get_currency_helper' );
1240 return charitable_get_currency_helper();
1241 }
1242
1243 /**
1244 * Returns the instance of Charitable_Locations.
1245 *
1246 * @deprecated 1.6.0
1247 *
1248 * @since 1.2.0 Deprecated.
1249 */
1250 public function get_location_helper() {
1251 charitable_get_deprecated()->deprecated_function( __METHOD__, '1.2.0', 'charitable_get_location_helper' );
1252 return charitable_get_location_helper();
1253 }
1254
1255 /**
1256 * Determine if we use Stripe core files or use an addon if that's installed and active. Checks for USE_NEW_STRIPE first.
1257 *
1258 * @since 1.7.0
1259 *
1260 * @return boolean
1261 */
1262 public function load_core_stripe() {
1263
1264 if ( false !== ( defined( 'USE_NEW_STRIPE' ) && USE_NEW_STRIPE ) ) {
1265 return true;
1266 }
1267
1268 // check and see if the core Stripe AM gateway is an active gateway, and if it is not, don't load the core stripe JS, etc.
1269 if ( class_exists( 'Charitable_Gateways' ) && class_exists( 'Charitable_Gateway_Stripe_AM' ) && ! Charitable_Gateways::get_instance()->is_active_gateway( Charitable_Gateway_Stripe_AM::get_gateway_id() ) ) {
1270 return false;
1271 }
1272
1273 return true;
1274 }
1275
1276 /**
1277 * Determine Stripe Connect is active.
1278 *
1279 * @since 1.7.0
1280 *
1281 * @return boolean
1282 */
1283 public function is_stripe_connect_addon() {
1284
1285 if ( in_array( 'charitable-stripe/charitable-stripe.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) { // phpcs:ignore
1286 return true;
1287 }
1288
1289 return false;
1290 }
1291
1292 /**
1293 * Action links for the plugins page.
1294 *
1295 * @since 1.7.0
1296 *
1297 * @param array $actions An array of existing actions.
1298 * @param string $plugin_file The plugin file we are modifying.
1299 * @param boolean $action_links An array of action links to add.
1300 * @return array An array of action links.
1301 */
1302 public function plugin_action_links( $actions, $plugin_file, $action_links = false ) {
1303
1304 if ( ! charitable_is_pro() ) :
1305
1306 // if this isn't pro, add the upgrade link which is visible on the plugins page until the plugin name.
1307
1308 $action_links = array(
1309 'proupgrade' => array(
1310 // Translators: This is an action link users can click to purchase a license for All in One SEO Pro.
1311 'label' => __( 'Upgrade to Pro', 'charitable' ),
1312 'url' => 'https://wpcharitable.com/lite-vs-pro/?utm_source=WordPress&utm_campaign=WP+Charitable&utm_medium=Action+Link+Plugins+Page&utm_content=Upgrade+to+Pro',
1313 ),
1314 );
1315
1316 endif;
1317
1318 if ( isset( $actions['edit'] ) ) {
1319 unset( $actions['edit'] );
1320 }
1321
1322 return $this->parse_action_links( $actions, $plugin_file, $action_links, 'before' );
1323 }
1324
1325 /**
1326 * Parse the action links.
1327 *
1328 * @since 1.7.0
1329 *
1330 * @param array $actions An array of existing actions.
1331 * @param string $plugin_file The plugin file we are modifying.
1332 * @param array $action_links An array of action links to add.
1333 * @param string $position The position to add the action links.
1334 * @return array
1335 */
1336 protected function parse_action_links( $actions, $plugin_file, $action_links = array(), $position = 'after' ) {
1337 if ( empty( $this->plugin ) ) {
1338 $this->plugin = CHARITABLE_PLUGIN_BASENAME;
1339 }
1340
1341 if ( $this->plugin === $plugin_file && ! empty( $action_links ) ) {
1342 foreach ( $action_links as $key => $value ) {
1343 $link = array(
1344 $key => '<a href="' . $value['url'] . '">' . $value['label'] . '</a>',
1345 );
1346
1347 $actions = 'after' === $position ? array_merge( $actions, $link ) : array_merge( $link, $actions );
1348 }
1349 }
1350
1351 return $actions;
1352 }
1353
1354 /**
1355 * Set up licensing for pro.
1356 *
1357 * @since 1.7.0
1358 *
1359 * @return void
1360 */
1361 public function setup_licensing() {
1362 charitable_get_helper( 'licenses' )->register_licensed_product(
1363 self::NAME,
1364 self::AUTHOR,
1365 self::VERSION,
1366 __FILE__
1367 );
1368 }
1369
1370 /**
1371 * Deativates and/or warnings about any Charitable verison conflicts.
1372 *
1373 * @since 1.7.0
1374 */
1375 public function check_for_version_conflicts() {
1376
1377 // Check for Charitable Stripe < 1.5.0.
1378 if ( in_array( 'charitable-stripe/charitable-stripe.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) { // phpcs:ignore
1379 if ( class_exists( 'Charitable_Stripe' ) && version_compare( Charitable_Stripe::VERSION, '1.5.0', '<' ) ) { // phpcs:ignore
1380 // manually deactivate plugin.
1381 $found = false;
1382 $current = get_option( 'active_plugins', array() );
1383 $key = array_search( 'charitable-stripe/charitable-stripe.php', $current, true );
1384 if ( false !== $key ) {
1385 $found = true;
1386
1387 unset( $current[ $key ] );
1388 update_option( 'active_plugins', $current );
1389
1390 // translators: %s is the version number.
1391 $message = sprintf( __( '<p><strong style="color:red;">NOTICE:</strong> <strong>Charitable</strong> detected an out-of-date and incompatible version of <strong>Charitable Stripe</strong> addon. To avoid issues with Stripe donations, <strong>Charitable Stripe</strong> %s was deactivated. Please update <strong>Charitable Stripe</strong> to version 1.5.0 or higher.</p><p>You can get the latest version of <strong>Charitable Stripe</strong> through your <a href="https://www.wpcharitable.com/account/" target="_blank">Charitable account at wpcharitable.com</a>. All active licenses are accessible in the "My Downloads" tab.</p>', 'charitable' ), Charitable_Stripe::VERSION );
1392
1393 wp_die( $message . ' <a href="' . admin_url( 'plugins.php' ) . '">Click here to return to the plugins screen.</a>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1394 }
1395 add_option( 'charitable_version_warning', $message );
1396 } else {
1397 delete_option( 'charitable_version_warning' );
1398 }
1399 }
1400 }
1401
1402 /**
1403 * Set initial timestamps for rotating menu items if they're already installed.
1404 * This ensures the 7-day countdown starts immediately for already-installed plugins.
1405 *
1406 * @since 1.8.8
1407 *
1408 * @return void
1409 */
1410 private function set_initial_rotating_menu_timestamps() {
1411 // Get the rotating menu items configuration
1412 $admin_pages = Charitable_Admin_Pages::get_instance();
1413
1414 try {
1415 $reflection = new ReflectionClass( $admin_pages );
1416 $method = $reflection->getMethod( 'get_marketing_rotation_items' );
1417 $method->setAccessible( true );
1418 $items = $method->invoke( $admin_pages );
1419 } catch ( ReflectionException $e ) {
1420 // Fallback if reflection fails in future PHP versions
1421 $items = array();
1422 }
1423
1424 $current_time = time();
1425
1426 foreach ( $items as $item ) {
1427 $option_name = $item['option_name'] ?? '';
1428 $option_key = $item['option_key'] ?? '';
1429 $plugin_file = $item['plugin_file'] ?? '';
1430
1431 if ( empty( $option_name ) || empty( $plugin_file ) ) {
1432 continue;
1433 }
1434
1435 // Check if the plugin is installed (file exists in plugins directory)
1436 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
1437 if ( file_exists( $plugin_path ) ) {
1438 // Plugin is installed, check if we already have a timestamp
1439 $existing_option = get_option( $option_name );
1440 $has_timestamp = false;
1441
1442 if ( is_array( $existing_option ) ) {
1443 $has_timestamp = isset( $existing_option[ $option_key ] ) && is_numeric( $existing_option[ $option_key ] );
1444 } elseif ( is_numeric( $existing_option ) ) {
1445 $has_timestamp = true;
1446 }
1447
1448 // Only set timestamp if one doesn't already exist
1449 if ( ! $has_timestamp ) {
1450 if ( is_array( $existing_option ) ) {
1451 $existing_option[ $option_key ] = $current_time;
1452 update_option( $option_name, $existing_option );
1453 } else {
1454 update_option( $option_name, array( $option_key => $current_time ) );
1455 }
1456 }
1457 }
1458 }
1459 }
1460 }
1461
1462 $charitable = new Charitable();
1463
1464 endif;
1465