PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
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
← All changes | charitable.php +646 -115 1.6.7 → 1.8.13.1 View file →
@@ -1,22 +1,25 @@
1 1 <?php
2 2 /**
3 - * Plugin Name: Charitable
4 - * Plugin URI: https://www.wpcharitable.com
5 - * Description: The WordPress fundraising alternative for non-profits, created to help non-profits raise money on their own website.
6 - * Version: 1.6.7
7 - * Author: WP Charitable
8 - * Author URI: https://wpcharitable.com
9 - * Requires at least: 4.1
10 - * Tested up to: 5.0.0
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.13.1
7 + * Author: Charitable Donations & Fundraising Team
8 + * Author URI: https://wpcharitable.com
9 + * Requires at least: 5.9
10 + * Requires PHP: 7.4
11 + * Stable tag: 1.8.13.1
12 + * License: GPLv2 or later
13 + * License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
11 14 *
12 - * Text Domain: charitable
13 - * Domain Path: /i18n/languages/
15 + * Text Domain: charitable
16 + * Domain Path: /i18n/languages/
14 17 *
15 - * @package Charitable
16 - * @author Eric Daams
17 - * @copyright Copyright (c) 2018, Studio 164a
18 - * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
18 + * @package Charitable
19 + * @author David Bisset
20 + * @copyright Copyright (c) 2026, WP Charitable LLC
21 + * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
19 22 */
20 23
21 24 // Exit if accessed directly.
22 25 if ( ! defined( 'ABSPATH' ) ) {
@@ -25,40 +28,28 @@
25 28
26 29 if ( ! class_exists( 'Charitable' ) ) :
27 30
28 31 /**
29 - * Main Charitable class
30 - *
31 - * @version 1.6.2
32 + * Main Charitable class.
32 33 */
33 34 class Charitable {
34 35
35 - /**
36 - * Plugin version.
37 - *
38 - * @var string
39 - */
40 - const VERSION = '1.6.7';
36 + /** The product name. */
37 + const NAME = 'Charitable';
41 38
42 - /**
43 - * Version of database schema.
44 - *
45 - * @var string A date in the format: YYYYMMDD
46 - */
39 + /** The plugin author name. */
40 + const AUTHOR = 'WP Charitable';
41 +
42 + /* Plugin version. */
43 + const VERSION = '1.8.13.1';
44 +
45 + /* Version of database schema. */
47 46 const DB_VERSION = '20180522';
48 47
49 - /**
50 - * Campaign post type.
51 - *
52 - * @var string
53 - */
48 + /* Campaign post type. */
54 49 const CAMPAIGN_POST_TYPE = 'campaign';
55 50
56 - /**
57 - * Donation post type.
58 - *
59 - * @var string
60 - */
51 + /* Donation post type. */
61 52 const DONATION_POST_TYPE = 'donation';
62 53
63 54 /**
64 55 * Single instance of this class.
@@ -115,8 +106,17 @@
115 106 */
116 107 private $classmap;
117 108
118 109 /**
110 + * Plugin.
111 + *
112 + * @since 1.8.0
113 + *
114 + * @var array
115 + */
116 + private $plugin;
117 +
118 + /**
119 119 * Create class instance.
120 120 *
121 121 * @since 1.0.0
122 122 */
@@ -124,8 +124,26 @@
124 124 $this->directory_path = plugin_dir_path( __FILE__ );
125 125 $this->directory_url = plugin_dir_url( __FILE__ );
126 126 $this->includes_path = $this->directory_path . 'includes/';
127 127
128 + define( 'CHARITABLE_DIRECTORY_PATH', plugin_dir_path( __FILE__ ) );
129 + define( 'CHARITABLE_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
130 + define( 'CHARITABLE_MARKETING_URL', 'https://wpcharitable.com/' );
131 + if ( ! defined( 'CHARITABLE_ANALYSIS_API_URL' ) ) {
132 + define( 'CHARITABLE_ANALYSIS_API_URL', apply_filters( 'charitable_analysis_api_url', 'https://nonprofitscore.org/api/v1/charitable/recommendations' ) );
133 + }
134 + // No credential is shipped. Each site earns a runtime-minted, quota-limited per-site token from
135 + // the register endpoint (see Charitable_Site_Analysis::register()); the token is not in plugin source.
136 + if ( ! defined( 'CHARITABLE_ANALYSIS_REGISTER_URL' ) ) {
137 + define( 'CHARITABLE_ANALYSIS_REGISTER_URL', apply_filters( 'charitable_analysis_register_url', 'https://nonprofitscore.org/api/v1/charitable/register' ) );
138 + }
139 + // Note: CHARITABLE_ANALYSIS_SALT is intentionally NOT defined here. It would need wp_salt(),
140 + // which lives in pluggable.php and is not loaded yet at plugin-construct time. The salt is
141 + // resolved lazily in Charitable_Site_Analysis::gather_config() (admin-ajax time), and a host
142 + // may still pre-define the CHARITABLE_ANALYSIS_SALT constant (e.g. in wp-config.php).
143 +
144 + define( 'CHARITABLE_BUILDER_SHOW_LEGACY_EDIT_LINKS', true ); // 1.8.0 specific.
145 +
128 146 spl_autoload_register( array( $this, 'autoloader' ) );
129 147
130 148 $this->load_dependencies();
131 149
@@ -131,9 +149,28 @@
131 149
132 150 register_activation_hook( __FILE__, array( $this, 'activate' ) );
133 151 register_deactivation_hook( __FILE__, array( $this, 'deactivate' ) );
134 152
135 - add_action( 'plugins_loaded', array( $this, 'start' ), 1 );
153 + add_filter( 'plugin_action_links_' . CHARITABLE_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ), 99, 2 );
154 + add_action( 'plugins_loaded', array( $this, 'start' ), 3 );
155 +
156 + add_action( 'plugins_loaded', array( $this, 'check_for_version_conflicts' ), 2 );
157 +
158 + // Run upgrade-server callback before any redirect (e.g. login) can run. Request from connect-callback.php.
159 + if ( defined( 'CHARITABLE_CONNECT_CALLBACK' ) && CHARITABLE_CONNECT_CALLBACK ) {
160 + add_action( 'init', array( $this, 'run_connect_callback' ), -9999 );
161 + }
162 +
163 + // Initialize error handler early.
164 + add_action(
165 + 'plugins_loaded',
166 + function () {
167 + require_once $this->get_path( 'includes' ) . 'class-charitable-error-handler.php';
168 + $error_handler = new Charitable_Error_Handler();
169 + $error_handler->init();
170 + },
171 + 1
172 + );
136 173 }
137 174
138 175 /**
139 176 * Returns the original instance of this class.
@@ -166,8 +203,10 @@
166 203
167 204 /* Set up the registry and instantiate objects we need straight away. */
168 205 $this->registry();
169 206
207 + $this->setup_licensing();
208 +
170 209 $this->maybe_start_ajax();
171 210
172 211 $this->attach_hooks_and_filters();
173 212
@@ -188,29 +227,98 @@
188 227 private function load_dependencies() {
189 228 $includes_path = $this->get_path( 'includes' );
190 229
191 230 /* Load files with hooks & functions. Classes are autoloaded. */
192 - require_once( $includes_path . 'charitable-core-functions.php' );
193 - require_once( $includes_path . 'api/charitable-api-functions.php' );
194 - require_once( $includes_path . 'campaigns/charitable-campaign-functions.php' );
195 - require_once( $includes_path . 'campaigns/charitable-campaign-hooks.php' );
196 - require_once( $includes_path . 'compat/charitable-compat-functions.php' );
197 - require_once( $includes_path . 'currency/charitable-currency-functions.php' );
198 - require_once( $includes_path . 'deprecated/charitable-deprecated-functions.php' );
199 - require_once( $includes_path . 'donations/charitable-donation-hooks.php' );
200 - require_once( $includes_path . 'donations/charitable-donation-functions.php' );
201 - require_once( $includes_path . 'emails/charitable-email-hooks.php' );
202 - require_once( $includes_path . 'endpoints/charitable-endpoints-functions.php' );
203 - require_once( $includes_path . 'privacy/charitable-privacy-functions.php' );
204 - require_once( $includes_path . 'public/charitable-template-helpers.php' );
205 - require_once( $includes_path . 'shortcodes/charitable-shortcodes-hooks.php' );
206 - require_once( $includes_path . 'upgrades/charitable-upgrade-hooks.php' );
207 - require_once( $includes_path . 'users/charitable-user-functions.php' );
208 - require_once( $includes_path . 'user-management/charitable-user-management-hooks.php' );
209 - require_once( $includes_path . 'utilities/charitable-utility-functions.php' );
231 + require_once $includes_path . 'class-charitable-log-muter.php';
232 + require_once $includes_path . 'charitable-core-functions.php';
233 + require_once $includes_path . 'api/charitable-api-functions.php';
234 + require_once $includes_path . 'campaigns/charitable-campaign-functions.php';
235 + require_once $includes_path . 'campaigns/charitable-campaign-hooks.php';
236 + require_once $includes_path . 'compat/charitable-compat-functions.php';
237 + require_once $includes_path . 'currency/charitable-currency-functions.php';
238 + require_once $includes_path . 'deprecated/charitable-deprecated-functions.php';
239 + require_once $includes_path . 'donations/charitable-donation-hooks.php';
240 + require_once $includes_path . 'donations/charitable-donation-functions.php';
241 + require_once $includes_path . 'emails/charitable-email-hooks.php';
242 + require_once $includes_path . 'endpoints/charitable-endpoints-functions.php';
243 + require_once $includes_path . 'privacy/charitable-privacy-functions.php';
244 + require_once $includes_path . 'public/charitable-template-helpers.php';
245 + require_once $includes_path . 'shortcodes/charitable-shortcodes-hooks.php';
246 + require_once $includes_path . 'tracking/charitable-tracking-hooks.php'; // 1.8.12 - MUST be outside the admin gate; see the file header.
247 + require_once $includes_path . 'addons-directory/charitable-addons-catalogue-hooks.php'; // 1.8.13.1 - MUST be outside the admin gate; see the file header.
248 + require_once $includes_path . 'abilities/charitable-abilities-hooks.php'; // 1.8.13 - MUST be outside the admin gate: abilities register on REST and WP-CLI requests, neither of which is is_admin().
249 + require_once $includes_path . 'upgrades/charitable-upgrade-hooks.php';
250 + require_once $includes_path . 'users/charitable-user-functions.php';
251 + require_once $includes_path . 'user-management/charitable-user-management-hooks.php';
252 + require_once $includes_path . 'utilities/charitable-utility-functions.php';
253 + require_once $includes_path . 'elementor/charitable-elementor-hooks.php';
254 +
255 + // Load security hooks on both admin and frontend (CAPTCHA needs to work on frontend).
256 + require_once $this->get_path( 'includes', true ) . 'admin/security/charitable-security-hooks.php';
257 +
258 + /* Load vendor/autoload.php - our modified platform_check.php and autoload_real.php will handle conditional loading */
259 + require_once $this->get_path( 'directory' ) . 'vendor/autoload.php';
260 +
261 + /* Square Compatibility Wrapper - always load this regardless of PHP version */
262 + require_once $includes_path . 'square/class-charitable-square-compatibility.php';
263 +
264 + /* Initialize Square conditionally - will check for compatibility internally */
265 + Charitable_Square_Compatibility::init();
266 +
267 + if ( $this->load_core_stripe() ) :
268 +
269 + /* Interfaces */
270 + require_once $includes_path . 'stripe/interfaces/interface-charitable-stripe-gateway-processor.php';
271 +
272 + /* Abstract classes */
273 + require_once $includes_path . 'stripe/abstracts/class-charitable-stripe-gateway-processor.php';
274 +
275 + /* Classes */
276 + require_once $includes_path . 'stripe/donations/class-charitable-stripe-donation-log.php';
277 + require_once $includes_path . 'stripe/donations/class-charitable-stripe-recurring-donation-log.php';
278 + require_once $includes_path . 'stripe/i18n/class-charitable-stripe-i18n.php';
279 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-connected-customer.php';
280 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-customer.php';
281 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-gateway-processor-payment-intents.php';
282 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-payment-intent.php';
283 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-plan.php';
284 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-product.php';
285 +
286 + require_once $includes_path . 'stripe/connect/charitable-stripe-connect-functions.php';
287 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-gateway-processor-checkout.php';
288 +
289 + /* Functions & Hooks */
290 + require_once $includes_path . 'stripe/charitable-stripe-core-functions.php';
291 + require_once $includes_path . 'stripe/donations/charitable-stripe-donation-functions.php';
292 + require_once $includes_path . 'stripe/gateway/charitable-stripe-gateway-hooks.php';
293 +
294 + /* webhooks */
295 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-webhook-api.php';
296 + require_once $includes_path . 'stripe/gateway/class-charitable-stripe-webhook-processor.php';
297 +
298 + endif;
299 +
300 + /* Load PayPal Commerce Gateway */
301 + require_once $includes_path . 'gateways/paypal-commerce/charitable-paypal-commerce.php';
210 302 }
211 303
212 304 /**
305 + * Load the template functions after theme is loaded.
306 + *
307 + * This gives themes time to override the functions.
308 + *
309 + * @since 1.6.10
310 + *
311 + * @return void
312 + */
313 + public function load_template_files() {
314 + $includes_path = $this->get_path( 'includes' );
315 +
316 + require_once $includes_path . 'public/charitable-template-functions.php';
317 + require_once $includes_path . 'public/charitable-template-hooks.php';
318 + }
319 +
320 + /**
213 321 * Dynamically loads the class attempting to be instantiated elsewhere in the
214 322 * plugin by looking at the $class_name parameter being passed as an argument.
215 323 *
216 324 * @since 1.5.0
@@ -230,18 +338,20 @@
230 338 }
231 339
232 340 /* Autogenerated class map. */
233 341 if ( ! isset( $this->classmap ) ) {
234 - $this->classmap = include( 'includes/autoloader/charitable-class-map.php' );
342 + $this->classmap = include 'includes/autoloader/charitable-class-map.php';
343 +
344 + $this->classmap['Charitable_Gateway_Stripe_AM'] = 'gateways/class-charitable-gateway-stripe-am.php';
345 + $this->classmap['Charitable_Gateway_Stripe'] = 'gateways/class-charitable-gateway-stripe-am.php';
235 346 }
236 347
237 348 $file_path = isset( $this->classmap[ $class_name ] ) ? $this->get_path( 'includes' ) . $this->classmap[ $class_name ] : false;
238 349
239 350 if ( false !== $file_path && file_exists( $file_path ) && is_file( $file_path ) ) {
240 - require_once( $file_path );
351 + require_once $file_path;
241 352 return true;
242 353 }
243 -
244 354 return false;
245 355 }
246 356
247 357 /**
@@ -263,10 +373,14 @@
263 373 $this->registry->register_object( Charitable_Widgets::get_instance() );
264 374 $this->registry->register_object( Charitable_Licenses::get_instance() );
265 375 $this->registry->register_object( Charitable_User_Dashboard::get_instance() );
266 376 $this->registry->register_object( Charitable_Locations::get_instance() );
377 + $this->registry->register_object( Charitable_Currency::get_instance() );
378 + $this->registry->register_object( Charitable_Notifications::get_instance() );
267 379
268 - $this->registry->register_object( new Charitable_Privacy );
380 + $this->registry->register_object( new Charitable_Privacy() );
381 + $this->registry->register_object( new Charitable_Debugging() );
382 + $this->registry->register_object( new Charitable_Locale() );
269 383 }
270 384
271 385 return $this->registry;
272 386 }
@@ -282,13 +396,14 @@
282 396 add_action( 'wpmu_new_blog', array( $this, 'maybe_activate_charitable_on_new_site' ) );
283 397 add_action( 'plugins_loaded', array( $this, 'charitable_install' ), 100 );
284 398 add_action( 'plugins_loaded', array( $this, 'charitable_start' ), 100 );
285 399 add_action( 'plugins_loaded', array( $this, 'endpoints' ), 100 );
286 - add_action( 'plugins_loaded', array( $this, 'donation_fields' ), 100 );
287 - add_action( 'plugins_loaded', array( $this, 'campaign_fields' ), 100 );
400 + add_action( 'init', array( $this, 'donation_fields' ), 100 );
401 + add_action( 'init', array( $this, 'campaign_fields' ), 100 );
288 402 add_action( 'plugins_loaded', array( $this, 'register_donormeta_table' ) );
289 403 add_action( 'plugins_loaded', 'charitable_load_compat_functions' );
290 404 add_action( 'setup_theme', array( 'Charitable_Customizer', 'start' ) );
405 + add_action( 'after_setup_theme', array( $this, 'load_template_files' ) );
291 406 add_action( 'wp_enqueue_scripts', array( $this, 'maybe_start_qunit' ), 100 );
292 407 add_action( 'rest_api_init', 'charitable_register_api_routes' );
293 408
294 409 /**
@@ -295,11 +410,78 @@
295 410 * We do this on priority 20 so that any functionality that is loaded on init (such
296 411 * as addons) has a chance to run before the event.
297 412 */
298 413 add_action( 'init', array( $this, 'do_charitable_actions' ), 20 );
414 +
415 + if ( $this->load_core_stripe() ) :
416 +
417 + /**
418 + * Set up scripts & stylesheets & enqueue them when appropriate.
419 + */
420 + add_action( 'charitable_form_after_fields', array( $this, 'maybe_setup_stripe_scripts_in_donation_form' ) );
421 + add_action( 'charitable_campaign_loop_after', array( $this, 'maybe_setup_stripe_scripts_in_campaign_loop' ) );
422 +
423 + endif;
299 424 }
300 425
426 +
301 427 /**
428 + * Load Stripe JS, as well as our handling scripts.
429 + *
430 + * @since 1.4.0
431 + *
432 + * @return boolean
433 + */
434 + public function enqueue_stripe_scripts() {
435 + if ( ! Charitable_Gateways::get_instance()->is_active_gateway( Charitable_Gateway_Stripe_AM::get_gateway_id() ) ) {
436 + return false;
437 + }
438 +
439 + wp_enqueue_script( 'charitable-stripe' );
440 +
441 + return true;
442 + }
443 +
444 + /**
445 + * Load Stripe JS or Stripe Checkout, as well as our handling script.
446 + *
447 + * @uses Charitable_Stripe::enqueue_scripts()
448 + *
449 + * @since 1.4.0
450 + *
451 + * @param Charitable_Donation_Form $form The current form object.
452 + * @return boolean
453 + */
454 + public function maybe_setup_stripe_scripts_in_donation_form( $form ) {
455 + if ( ! is_a( $form, 'Charitable_Donation_Form' ) ) {
456 + return false;
457 + }
458 +
459 + if ( 'make_donation' !== $form->get_form_action() ) {
460 + return false;
461 + }
462 +
463 + return $this->enqueue_stripe_scripts();
464 + }
465 +
466 + /**
467 + * Enqueue the Stripe JS/Checkout scripts after a campaign loop if modal donations are in use.
468 + *
469 + * @uses Charitable_Stripe::enqueue_scripts()
470 + *
471 + * @since 1.4.0
472 + *
473 + * @return boolean
474 + */
475 + public function maybe_setup_stripe_scripts_in_campaign_loop() {
476 + if ( 'modal' !== charitable_get_option( 'donation_form_display', 'separate_page' ) ) {
477 + return false;
478 + }
479 +
480 + return $this->enqueue_stripe_scripts();
481 + }
482 +
483 + /**
302 484 * Checks whether we're in the admin area and if so, loads the admin-only functionality.
303 485 *
304 486 * @since 1.0.0
305 487 *
@@ -309,19 +491,70 @@
309 491 if ( ! is_admin() ) {
310 492 return false;
311 493 }
312 494
313 - require_once( $this->get_path( 'admin' ) . 'class-charitable-admin.php' );
314 - require_once( $this->get_path( 'admin' ) . 'charitable-admin-hooks.php' );
495 + // load blocks/Gutenberg related.
496 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin-blocks.php';
315 497
498 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin.php';
499 + require_once $this->get_path( 'admin' ) . 'charitable-admin-hooks.php';
500 +
501 + // load campaign builder.
502 + require_once $this->get_path( 'admin' ) . 'campaign-builder/util.php';
503 + require_once $this->get_path( 'admin' ) . 'campaign-builder/functions.php';
504 + require_once $this->get_path( 'admin' ) . 'campaign-builder/ajax-actions.php';
505 + require_once $this->get_path( 'admin' ) . 'campaign-builder/functions-legacy.php';
506 + require_once $this->get_path( 'admin' ) . 'campaign-builder/debug.php';
507 + require_once $this->get_path( 'admin' ) . 'campaign-builder/access.php';
508 + require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-fields.php';
509 + require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-form-fields.php';
510 +
511 + // connect for upgrade.wpcharitable.com.
512 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin-connect.php';
513 +
514 + // misc.
515 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin-pointers.php';
516 + require_once $this->get_path( 'admin' ) . 'plugins/charitable-admin-plugin-hooks.php';
517 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin-getting-started.php';
518 +
519 + // Site Analysis / Recommendations tab.
520 + require_once $this->get_path( 'admin' ) . 'site-analysis/class-charitable-site-analysis.php';
521 + Charitable_Site_Analysis::get_instance();
522 +
316 523 $admin = Charitable_Admin::get_instance();
524 + $this->registry->register_object( $admin );
317 525
318 - $this->registry->register_object( $admin );
526 + // Stripe.
527 + require_once $this->get_path( 'includes' ) . 'stripe/admin/class-charitable-stripe-admin.php';
528 + new Charitable_Stripe_Admin();
319 529
320 530 return $admin;
321 531 }
322 532
323 533 /**
534 + * Run the upgrade-server connect callback on init before any redirect (e.g. to login) can run.
535 + * Only registered when CHARITABLE_CONNECT_CALLBACK is defined (request came from connect-callback.php).
536 + *
537 + * @since 1.8.9.6
538 + */
539 + public function run_connect_callback() {
540 + ini_set( 'display_errors', '0' );
541 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
542 + require_once ABSPATH . 'wp-admin/includes/file.php';
543 + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
544 + $connect_file = $this->get_path( 'admin' ) . 'class-charitable-admin-connect.php';
545 + if ( file_exists( $connect_file ) ) {
546 + require_once $connect_file;
547 + }
548 + if ( ! class_exists( 'Charitable_Admin_Connect' ) ) {
549 + header( 'Content-Type: application/json' );
550 + wp_send_json( array( 'success' => false, 'data' => array( 'message' => 'Charitable not loaded.' ) ) );
551 + }
552 + Charitable_Admin_Connect::get_instance()->process();
553 + exit;
554 + }
555 +
556 + /**
324 557 * Checks whether we're on the public-facing side and if so, loads the public-facing functionality.
325 558 *
326 559 * @since 1.0.0
327 560 *
@@ -331,10 +564,18 @@
331 564 if ( is_admin() && ! $this->is_ajax() ) {
332 565 return false;
333 566 }
334 567
335 - require_once( $this->get_path( 'public' ) . 'class-charitable-public.php' );
568 + require_once $this->get_path( 'public' ) . 'class-charitable-public.php';
336 569
570 + // Campaign Builder related (v1.8.0).
571 + require_once $this->get_path( 'admin' ) . 'campaign-builder/class-campaign-builder-preview.php';
572 + require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-fields.php';
573 + require_once $this->get_path( 'admin' ) . 'campaign-builder/class-builder-form-fields.php';
574 +
575 + // load blocks/Gutenberg related.
576 + require_once $this->get_path( 'admin' ) . 'class-charitable-admin-blocks.php';
577 +
337 578 $public = Charitable_Public::get_instance();
338 579
339 580 $this->registry->register_object( $public );
340 581
@@ -349,24 +590,51 @@
349 590 * @return boolean
350 591 */
351 592 public function maybe_start_qunit() {
352 593 /* Skip out early if ?qunit isn't included in the request. */
353 - if ( ! array_key_exists( 'qunit', $_GET ) ) {
594 + if ( ! array_key_exists( 'qunit', $_GET ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
354 595 return false;
355 596 }
356 597
357 - /* The unit tests have to exist. */
358 - if ( ! file_exists( $this->get_path( 'directory' ) . 'tests/qunit/tests.js' ) ) {
359 - return false;
360 - }
598 + /* The unit tests have to exist. */
599 + if ( ! file_exists( $this->get_path( 'directory' ) . 'tests/qunit/tests.js' ) ) {
600 + return false;
601 + }
361 602
362 - wp_register_script( 'qunit', 'https://code.jquery.com/qunit/qunit-2.3.3.js', array(), '2.3.3', true );
363 - /* Version: '20170615-15:44' */
364 - wp_register_script( 'qunit-tests', $this->get_path( 'directory', false ) . 'tests/qunit/tests.js', array( 'jquery-core', 'qunit' ), time(), true );
365 - wp_enqueue_script( 'qunit-tests' );
603 + /**
604 + * Register QUnit testing library.
605 + *
606 + * Note: These files must be bundled locally for WordPress.org compliance.
607 + * Download from:
608 + * - JS: https://code.jquery.com/qunit/qunit-2.3.3.js
609 + * - CSS: https://code.jquery.com/qunit/qunit-2.3.3.css
610 + * Save to:
611 + * - assets/js/libraries/qunit-2.3.3.js
612 + * - assets/css/libraries/qunit-2.3.3.css
613 + *
614 + * @since 1.0.0
615 + * @version 1.8.9.1
616 + */
617 + wp_register_script(
618 + 'qunit',
619 + $this->get_path( 'directory', false ) . 'assets/js/libraries/qunit-2.3.3.js',
620 + array(),
621 + '2.3.3',
622 + true
623 + );
366 624
367 - wp_register_style( 'qunit', 'https://code.jquery.com/qunit/qunit-2.3.3.css', array(), '2.3.3', 'all' );
368 - wp_enqueue_style( 'qunit' );
625 + /* Version: '20170615-15:44' */
626 + wp_register_script( 'qunit-tests', $this->get_path( 'directory', false ) . 'tests/qunit/tests.js', array( 'jquery', 'qunit' ), time(), true );
627 + wp_enqueue_script( 'qunit-tests' );
628 +
629 + wp_register_style(
630 + 'qunit',
631 + $this->get_path( 'directory', false ) . 'assets/css/libraries/qunit-2.3.3.css',
632 + array(),
633 + '2.3.3',
634 + 'all'
635 + );
636 + wp_enqueue_style( 'qunit' );
369 637 }
370 638
371 639 /**
372 640 * Checks whether the current request is an AJAX request.
@@ -390,10 +658,10 @@
390 658 if ( ! $this->is_ajax() ) {
391 659 return;
392 660 }
393 661
394 - require_once( $this->get_path( 'includes' ) . 'ajax/charitable-ajax-functions.php' );
395 - require_once( $this->get_path( 'includes' ) . 'ajax/charitable-ajax-hooks.php' );
662 + require_once $this->get_path( 'includes' ) . 'ajax/charitable-ajax-functions.php';
663 + require_once $this->get_path( 'includes' ) . 'ajax/charitable-ajax-hooks.php';
396 664 }
397 665
398 666 /**
399 667 * This method is fired after all plugins are loaded and simply fires the charitable_start hook.
@@ -411,9 +679,11 @@
411 679 /**
412 680 * Fires off an action right after Charitable is installed, allowing other
413 681 * plugins/themes to do something at this point.
414 682 *
415 - * @since 1.0.1
683 + * @since 1.0.1
684 + * @version 1.8.1.12 - Added transient for activation redirect.
685 + * @version 1.8.3 - Added call to add version and delete notifications.
416 686 *
417 687 * @return void
418 688 */
419 689 public function charitable_install() {
@@ -422,9 +692,9 @@
422 692 if ( ! $install ) {
423 693 return;
424 694 }
425 695
426 - require_once( $this->get_path( 'includes' ) . 'class-charitable-install.php' );
696 + require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-install.php';
427 697
428 698 Charitable_Install::finish_installing();
429 699
430 700 do_action( 'charitable_install' );
@@ -429,8 +699,28 @@
429 699
430 700 do_action( 'charitable_install' );
431 701
432 702 delete_transient( 'charitable_install' );
703 +
704 + // Store the date when the initial activation was performed.
705 + $type = function_exists( 'charitable_is_pro' ) && charitable_is_pro() ? 'pro' : 'lite';
706 + $activated = (array) get_option( 'charitable_activated', array() );
707 +
708 + // If no activation date is set, assume it's a new install and set the activation redirect transient.
709 + if ( false === $activated || empty( $activated ) ) {
710 + // Add transient to trigger redirect to the Welcome / Getting Started screen.
711 + set_transient( 'charitable_activation_redirect', true, 30 );
712 + }
713 +
714 + $this->add_version_to_upgraded_from();
715 +
716 + if ( empty( $activated[ $type ] ) ) {
717 + $activated[ $type ] = time();
718 + update_option( 'charitable_activated', $activated );
719 + }
720 +
721 + // Clear notifications.
722 + delete_option( 'charitable_notifications' );
433 723 }
434 724
435 725 /**
436 726 * Returns whether we are currently in the start phase of the plugin.
@@ -439,9 +729,9 @@
439 729 *
440 730 * @return boolean
441 731 */
442 732 public function is_start() {
443 - return current_filter() == 'charitable_start';
733 + return current_filter() == 'charitable_start'; // phpcs:ignore
444 734 }
445 735
446 736 /**
447 737 * Returns whether the plugin has already started.
@@ -450,9 +740,9 @@
450 740 *
451 741 * @return boolean
452 742 */
453 743 public function started() {
454 - return did_action( 'charitable_start' ) || current_filter() == 'charitable_start';
744 + return did_action( 'charitable_start' ) || current_filter() == 'charitable_start'; // phpcs:ignore
455 745 }
456 746
457 747 /**
458 748 * Returns whether the plugin is being activated.
@@ -461,9 +751,9 @@
461 751 *
462 752 * @return boolean
463 753 */
464 754 public function is_activation() {
465 - return current_filter() == 'activate_charitable/charitable.php';
755 + return current_filter() == 'activate_charitable/charitable.php'; // phpcs:ignore
466 756 }
467 757
468 758 /**
469 759 * Returns whether the plugin is being deactivated.
@@ -472,9 +762,9 @@
472 762 *
473 763 * @return boolean
474 764 */
475 765 public function is_deactivation() {
476 - return current_filter() == 'deactivate_charitable/charitable.php';
766 + return current_filter() == 'deactivate_charitable/charitable.php'; // phpcs:ignore
477 767 }
478 768
479 769 /**
480 770 * Returns plugin paths.
@@ -508,8 +798,12 @@
508 798 case 'templates':
509 799 $path = $base . 'templates/';
510 800 break;
511 801
802 + case 'plugin-directory':
803 + $path = WP_PLUGIN_DIR;
804 + break;
805 +
512 806 case 'directory':
513 807 $path = $base;
514 808 break;
515 809
@@ -539,8 +833,26 @@
539 833 return $version;
540 834 }
541 835
542 836 /**
837 + * Adds previous versions to the upgraded_from option.
838 + *
839 + * @since 1.8.3
840 + */
841 + public function add_version_to_upgraded_from() {
842 +
843 + $upgraded_from = get_option( 'charitable_version_upgraded_from', array() );
844 +
845 + if ( ! is_array( $upgraded_from ) ) {
846 + $upgraded_from = array();
847 + }
848 +
849 + $upgraded_from[] = $this->get_version();
850 +
851 + update_option( 'charitable_version_upgraded_from', $upgraded_from, false );
852 + }
853 +
854 + /**
543 855 * Get the campaign fields registry
544 856 *
545 857 * If the registry has not been set up yet, this will also
546 858 * perform the task of setting it up initially.
@@ -557,9 +869,9 @@
557 869
558 870 /* Register it immediately to avoid endless recursion. */
559 871 $this->registry->register_object( $campaign_fields );
560 872
561 - $fields = include( $this->get_path( 'includes' ) . 'fields/default-fields/campaign-fields.php' );
873 + $fields = include $this->get_path( 'includes' ) . 'fields/default-fields/campaign-fields.php';
562 874
563 875 foreach ( $fields as $key => $args ) {
564 876 $campaign_fields->register_field( new Charitable_Campaign_Field( $key, $args ) );
565 877 }
@@ -586,9 +898,9 @@
586 898
587 899 /* Register it immediately to avoid endless recursion. */
588 900 $this->registry->register_object( $donation_fields );
589 901
590 - $fields = include( $this->get_path( 'includes' ) . 'fields/default-fields/donation-fields.php' );
902 + $fields = include $this->get_path( 'includes' ) . 'fields/default-fields/donation-fields.php';
591 903
592 904 foreach ( $fields as $key => $args ) {
593 905 $donation_fields->register_field( new Charitable_Donation_Field( $key, $args ) );
594 906 }
@@ -620,21 +932,22 @@
620 932 * BEFORE the endpoint it builds on. In other words, move from
621 933 * most specific to least specific.
622 934 */
623 935 $endpoints = new Charitable_Endpoints();
624 - $endpoints->register( new Charitable_Campaign_Donation_Endpoint );
625 - $endpoints->register( new Charitable_Campaign_Widget_Endpoint );
626 - $endpoints->register( new Charitable_Campaign_Endpoint );
627 - $endpoints->register( new Charitable_Donation_Cancellation_Endpoint );
628 - $endpoints->register( new Charitable_Donation_Processing_Endpoint );
629 - $endpoints->register( new Charitable_Donation_Receipt_Endpoint );
630 - $endpoints->register( new Charitable_Email_Preview_Endpoint );
631 - $endpoints->register( new Charitable_Email_Verification_Endpoint );
632 - $endpoints->register( new Charitable_Forgot_Password_Endpoint );
633 - $endpoints->register( new Charitable_Reset_Password_Endpoint );
634 - $endpoints->register( new Charitable_Registration_Endpoint );
635 - $endpoints->register( new Charitable_Login_Endpoint );
636 - $endpoints->register( new Charitable_Profile_Endpoint );
936 + $endpoints->register( new Charitable_Campaign_Donation_Endpoint() );
937 + $endpoints->register( new Charitable_Campaign_Widget_Endpoint() );
938 + $endpoints->register( new Charitable_Campaign_Endpoint() );
939 + $endpoints->register( new Charitable_Donation_Cancellation_Endpoint() );
940 + $endpoints->register( new Charitable_Donation_Processing_Endpoint() );
941 + $endpoints->register( new Charitable_Donation_Receipt_Endpoint() );
942 + $endpoints->register( new Charitable_Email_Preview_Endpoint() );
943 + $endpoints->register( new Charitable_Email_Verification_Endpoint() );
944 + $endpoints->register( new Charitable_Forgot_Password_Endpoint() );
945 + $endpoints->register( new Charitable_Reset_Password_Endpoint() );
946 + $endpoints->register( new Charitable_Registration_Endpoint() );
947 + $endpoints->register( new Charitable_Login_Endpoint() );
948 + $endpoints->register( new Charitable_Profile_Endpoint() );
949 + $endpoints->register( new Charitable_Webhook_Listener_Endpoint() );
637 950
638 951 $this->registry->register_object( $endpoints );
639 952 }
640 953
@@ -648,9 +961,9 @@
648 961 *
649 962 * @param string $class The type of class to be retrieved.
650 963 * @return object
651 964 */
652 - public function get_registered_object( $class ) {
965 + public function get_registered_object( $class ) { // phpcs:ignore
653 966 return $this->registry->get( $class );
654 967 }
655 968
656 969 /**
@@ -701,16 +1014,22 @@
701 1014 /**
702 1015 * Filter the array of available Charitable table classes.
703 1016 *
704 1017 * @since 1.0.0
1018 + * @version 1.8.1
705 1019 *
706 1020 * @param array $tables List of tables as a key=>value array.
707 1021 */
708 - return apply_filters( 'charitable_db_tables', array(
709 - 'campaign_donations' => 'Charitable_Campaign_Donations_DB',
710 - 'donors' => 'Charitable_Donors_DB',
711 - 'donormeta' => 'Charitable_Donormeta_DB',
712 - ) );
1022 + return apply_filters(
1023 + 'charitable_db_tables',
1024 + array(
1025 + 'campaign_donations' => 'Charitable_Campaign_Donations_DB',
1026 + 'donors' => 'Charitable_Donors_DB',
1027 + 'donormeta' => 'Charitable_Donormeta_DB',
1028 + 'donation_activities' => 'Charitable_Donation_Activities_DB',
1029 + 'campaign_activities' => 'Charitable_Campaign_Activities_DB',
1030 + )
1031 + );
713 1032 }
714 1033
715 1034 /**
716 1035 * Maybe activate Charitable when a new site is added in a multisite network.
@@ -739,21 +1058,27 @@
739 1058 * or just the current site. Multisite only. Default is false.
740 1059 * @return void
741 1060 */
742 1061 public function activate( $network_wide = false ) {
743 - require_once( $this->get_path( 'includes' ) . 'class-charitable-install.php' );
1062 + require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-install.php';
744 1063
745 1064 if ( is_multisite() && $network_wide ) {
746 1065 global $wpdb;
747 1066
748 - foreach ( $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" ) as $blog_id ) {
1067 + foreach ( $wpdb->get_col( "SELECT blog_id FROM $wpdb->blogs" ) as $blog_id ) { // phpcs:ignore
749 1068 switch_to_blog( $blog_id );
750 - new Charitable_Install();
1069 + new Charitable_Install( $this->includes_path );
751 1070 restore_current_blog();
752 1071 }
753 1072 } else {
754 - new Charitable_Install();
1073 + new Charitable_Install( $this->includes_path );
755 1074 }
1075 +
1076 + // create or update the install date for future reference.
1077 + update_option( 'wpcharitable_activated_datetime', current_time( 'timestamp' ) ); // phpcs:ignore WordPress.DateTime.CurrentTimeTimestamp.Requested
1078 +
1079 + // Set initial timestamps for rotating menu items if they're already installed
1080 + $this->set_initial_rotating_menu_timestamps();
756 1081 }
757 1082
758 1083 /**
759 1084 * Runs on plugin deactivation.
@@ -764,9 +1089,9 @@
764 1089 *
765 1090 * @return void
766 1091 */
767 1092 public function deactivate() {
768 - require_once( $this->get_path( 'includes' ) . 'class-charitable-uninstall.php' );
1093 + require_once $this->get_path( 'includes' ) . 'plugin/class-charitable-uninstall.php';
769 1094 new Charitable_Uninstall();
770 1095 }
771 1096
772 1097 /**
@@ -776,11 +1101,11 @@
776 1101 *
777 1102 * @return void
778 1103 */
779 1104 public function do_charitable_actions() {
780 - if ( isset( $_REQUEST['charitable_action'] ) ) {
1105 + if ( isset( $_REQUEST['charitable_action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
781 1106
782 - $action = $_REQUEST['charitable_action'];
1107 + $action = esc_attr( $_REQUEST['charitable_action'] ); // phpcs:ignore
783 1108
784 1109 /**
785 1110 * Handle Charitable action.
786 1111 *
@@ -856,9 +1181,9 @@
856 1181 *
857 1182 * @param mixed $object Object to be registered.
858 1183 * @return void
859 1184 */
860 - public function register_object( $object ) {
1185 + public function register_object( $object ) { // phpcs:ignore
861 1186 charitable_get_deprecated()->deprecated_function(
862 1187 __METHOD__,
863 1188 '1.5.0',
864 1189 'charitable()->registry()->register_object()'
@@ -948,8 +1273,214 @@
948 1273 */
949 1274 public function get_location_helper() {
950 1275 charitable_get_deprecated()->deprecated_function( __METHOD__, '1.2.0', 'charitable_get_location_helper' );
951 1276 return charitable_get_location_helper();
1277 + }
1278 +
1279 + /**
1280 + * Determine if we use Stripe core files or use an addon if that's installed and active. Checks for USE_NEW_STRIPE first.
1281 + *
1282 + * @since 1.7.0
1283 + *
1284 + * @return boolean
1285 + */
1286 + public function load_core_stripe() {
1287 +
1288 + if ( false !== ( defined( 'USE_NEW_STRIPE' ) && USE_NEW_STRIPE ) ) {
1289 + return true;
1290 + }
1291 +
1292 + // 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.
1293 + 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() ) ) {
1294 + return false;
1295 + }
1296 +
1297 + return true;
1298 + }
1299 +
1300 + /**
1301 + * Determine Stripe Connect is active.
1302 + *
1303 + * @since 1.7.0
1304 + *
1305 + * @return boolean
1306 + */
1307 + public function is_stripe_connect_addon() {
1308 +
1309 + if ( in_array( 'charitable-stripe/charitable-stripe.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) { // phpcs:ignore
1310 + return true;
1311 + }
1312 +
1313 + return false;
1314 + }
1315 +
1316 + /**
1317 + * Action links for the plugins page.
1318 + *
1319 + * @since 1.7.0
1320 + *
1321 + * @param array $actions An array of existing actions.
1322 + * @param string $plugin_file The plugin file we are modifying.
1323 + * @param boolean $action_links An array of action links to add.
1324 + * @return array An array of action links.
1325 + */
1326 + public function plugin_action_links( $actions, $plugin_file, $action_links = false ) {
1327 +
1328 + if ( ! charitable_is_pro() ) :
1329 +
1330 + // if this isn't pro, add the upgrade link which is visible on the plugins page until the plugin name.
1331 +
1332 + $action_links = array(
1333 + 'proupgrade' => array(
1334 + // Translators: This is an action link users can click to purchase a license for All in One SEO Pro.
1335 + 'label' => __( 'Upgrade to Pro', 'charitable' ),
1336 + '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',
1337 + ),
1338 + );
1339 +
1340 + endif;
1341 +
1342 + if ( isset( $actions['edit'] ) ) {
1343 + unset( $actions['edit'] );
1344 + }
1345 +
1346 + return $this->parse_action_links( $actions, $plugin_file, $action_links, 'before' );
1347 + }
1348 +
1349 + /**
1350 + * Parse the action links.
1351 + *
1352 + * @since 1.7.0
1353 + *
1354 + * @param array $actions An array of existing actions.
1355 + * @param string $plugin_file The plugin file we are modifying.
1356 + * @param array $action_links An array of action links to add.
1357 + * @param string $position The position to add the action links.
1358 + * @return array
1359 + */
1360 + protected function parse_action_links( $actions, $plugin_file, $action_links = array(), $position = 'after' ) {
1361 + if ( empty( $this->plugin ) ) {
1362 + $this->plugin = CHARITABLE_PLUGIN_BASENAME;
1363 + }
1364 +
1365 + if ( $this->plugin === $plugin_file && ! empty( $action_links ) ) {
1366 + foreach ( $action_links as $key => $value ) {
1367 + $link = array(
1368 + $key => '<a href="' . $value['url'] . '">' . $value['label'] . '</a>',
1369 + );
1370 +
1371 + $actions = 'after' === $position ? array_merge( $actions, $link ) : array_merge( $link, $actions );
1372 + }
1373 + }
1374 +
1375 + return $actions;
1376 + }
1377 +
1378 + /**
1379 + * Set up licensing for pro.
1380 + *
1381 + * @since 1.7.0
1382 + *
1383 + * @return void
1384 + */
1385 + public function setup_licensing() {
1386 + charitable_get_helper( 'licenses' )->register_licensed_product(
1387 + self::NAME,
1388 + self::AUTHOR,
1389 + self::VERSION,
1390 + __FILE__
1391 + );
1392 + }
1393 +
1394 + /**
1395 + * Deativates and/or warnings about any Charitable verison conflicts.
1396 + *
1397 + * @since 1.7.0
1398 + */
1399 + public function check_for_version_conflicts() {
1400 +
1401 + // Check for Charitable Stripe < 1.5.0.
1402 + if ( in_array( 'charitable-stripe/charitable-stripe.php', apply_filters( 'active_plugins', get_option( 'active_plugins' ) ) ) ) { // phpcs:ignore
1403 + if ( class_exists( 'Charitable_Stripe' ) && version_compare( Charitable_Stripe::VERSION, '1.5.0', '<' ) ) { // phpcs:ignore
1404 + // manually deactivate plugin.
1405 + $found = false;
1406 + $current = get_option( 'active_plugins', array() );
1407 + $key = array_search( 'charitable-stripe/charitable-stripe.php', $current, true );
1408 + if ( false !== $key ) {
1409 + $found = true;
1410 +
1411 + unset( $current[ $key ] );
1412 + update_option( 'active_plugins', $current );
1413 +
1414 + // translators: %s is the version number.
1415 + $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 );
1416 +
1417 + wp_die( $message . ' <a href="' . admin_url( 'plugins.php' ) . '">Click here to return to the plugins screen.</a>' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1418 + }
1419 + add_option( 'charitable_version_warning', $message );
1420 + } else {
1421 + delete_option( 'charitable_version_warning' );
1422 + }
1423 + }
1424 + }
1425 +
1426 + /**
1427 + * Set initial timestamps for rotating menu items if they're already installed.
1428 + * This ensures the 7-day countdown starts immediately for already-installed plugins.
1429 + *
1430 + * @since 1.8.8
1431 + *
1432 + * @return void
1433 + */
1434 + private function set_initial_rotating_menu_timestamps() {
1435 + // Get the rotating menu items configuration
1436 + $admin_pages = Charitable_Admin_Pages::get_instance();
1437 +
1438 + try {
1439 + $reflection = new ReflectionClass( $admin_pages );
1440 + $method = $reflection->getMethod( 'get_marketing_rotation_items' );
1441 + $method->setAccessible( true );
1442 + $items = $method->invoke( $admin_pages );
1443 + } catch ( ReflectionException $e ) {
1444 + // Fallback if reflection fails in future PHP versions
1445 + $items = array();
1446 + }
1447 +
1448 + $current_time = time();
1449 +
1450 + foreach ( $items as $item ) {
1451 + $option_name = $item['option_name'] ?? '';
1452 + $option_key = $item['option_key'] ?? '';
1453 + $plugin_file = $item['plugin_file'] ?? '';
1454 +
1455 + if ( empty( $option_name ) || empty( $plugin_file ) ) {
1456 + continue;
1457 + }
1458 +
1459 + // Check if the plugin is installed (file exists in plugins directory)
1460 + $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
1461 + if ( file_exists( $plugin_path ) ) {
1462 + // Plugin is installed, check if we already have a timestamp
1463 + $existing_option = get_option( $option_name );
1464 + $has_timestamp = false;
1465 +
1466 + if ( is_array( $existing_option ) ) {
1467 + $has_timestamp = isset( $existing_option[ $option_key ] ) && is_numeric( $existing_option[ $option_key ] );
1468 + } elseif ( is_numeric( $existing_option ) ) {
1469 + $has_timestamp = true;
1470 + }
1471 +
1472 + // Only set timestamp if one doesn't already exist
1473 + if ( ! $has_timestamp ) {
1474 + if ( is_array( $existing_option ) ) {
1475 + $existing_option[ $option_key ] = $current_time;
1476 + update_option( $option_name, $existing_option );
1477 + } else {
1478 + update_option( $option_name, array( $option_key => $current_time ) );
1479 + }
1480 + }
1481 + }
1482 + }
952 1483 }
953 1484 }
954 1485
955 1486 $charitable = new Charitable();