PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.11.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.11.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
charitable / includes / upgrades / class-charitable-upgrade.php

class-charitable-upgrade.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.11.1, at includes/upgrades/class-charitable-upgrade.php

1,601 lines 43.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Upgrade class.
4 *
5 * The responsibility of this class is to manage migrations between versions of Charitable.
6 *
7 * @package Charitable/Classes/Charitable_Upgrade
8 * @copyright Copyright (c) 2023, WP Charitable LLC
9 * @license http://opensource.org/licenses/gpl-1.0.0.php GNU Public License
10 * @since 1.0.0
11 * @version 1.6.51
12 */
13
14 // Exit if accessed directly.
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 if ( ! class_exists( 'Charitable_Upgrade' ) ) :
20
21 /**
22 * Charitable_EDD_Upgrade
23 *
24 * @since 1.0.0
25 */
26 class Charitable_Upgrade {
27
28 /**
29 * The single class instance.
30 *
31 * @since 1.3.0
32 *
33 * @var Charitable_Upgrade
34 */
35 private static $instance = null;
36
37 /**
38 * Whether the "upgrade in progress" notice has been shown.
39 *
40 * @since 1.6.14
41 *
42 * @var boolean
43 */
44 private $notice_shown_for_upgrade_in_progress = false;
45
46 /**
47 * Current database version.
48 *
49 * @since 1.0.0
50 *
51 * @var false|string
52 */
53 protected $db_version;
54
55 /**
56 * Edge version.
57 *
58 * @since 1.0.0
59 *
60 * @var string
61 */
62 protected $edge_version;
63
64 /**
65 * Array of methods to perform when upgrading to specific versions.
66 *
67 * @since 1.0.0
68 *
69 * @var array
70 */
71 protected $upgrade_actions;
72
73 /**
74 * Option key for upgrade log.
75 *
76 * @since 1.0.0
77 *
78 * @var string
79 */
80 protected $upgrade_log_key = 'charitable_upgrade_log';
81
82 /**
83 * Option key for plugin version.
84 *
85 * @since 1.0.0
86 *
87 * @var string
88 */
89 protected $version_key = 'charitable_version';
90
91 /**
92 * Create and return the class object.
93 *
94 * @since 1.3.0
95 *
96 * @return Charitable_Upgrade
97 */
98 public static function get_instance() {
99 if ( is_null( self::$instance ) ) {
100 self::$instance = new self();
101 }
102
103 return self::$instance;
104 }
105
106 /**
107 * Manages the upgrade process.
108 *
109 * @since 1.0.0
110 *
111 * @param false|string $db_version The version we are upgrading from.
112 * @param string $edge_version The new version.
113 */
114 protected function __construct( $db_version = '', $edge_version = '' ) {
115 /**
116 * We are keeping this for the sake of backwards compatibility for
117 * extensions that extend Charitable_Upgrade.
118 */
119 if ( strlen( $db_version ) && strlen( $edge_version ) ) {
120 return $this->legacy_upgrade_mode( $db_version, $edge_version );
121 }
122 }
123
124 /**
125 * Initialize the upgrade actions.
126 *
127 * @since 1.0.0
128 *
129 * @return void
130 */
131 public function init() {
132 $this->upgrade_actions = array(
133 'update_upgrade_system' => array(
134 'version' => '1.3.0',
135 'message' => __( 'Charitable needs to update the database.', 'charitable' ),
136 'prompt' => true,
137 ),
138 'fix_donation_dates' => array(
139 'version' => '1.3.0',
140 'message' => __( 'Charitable needs to fix incorrect donation dates.', 'charitable' ),
141 'prompt' => true,
142 ),
143 'trigger_cron' => array(
144 'version' => '1.3.4',
145 'message' => '',
146 'prompt' => false,
147 'callback' => array( $this, 'trigger_cron' ),
148 ),
149 'flush_permalinks_140' => array(
150 'version' => '1.4.0',
151 'message' => '',
152 'prompt' => false,
153 'callback' => array( $this, 'flush_permalinks' ),
154 ),
155 'remove_campaign_manager_cap' => array(
156 'version' => '1.4.5',
157 'message' => '',
158 'prompt' => false,
159 'callback' => array( $this, 'remove_campaign_manager_cap' ),
160 ),
161 'fix_empty_campaign_end_date_meta' => array(
162 'version' => '1.4.11',
163 'message' => '',
164 'prompt' => false,
165 'callback' => array( $this, 'fix_empty_campaign_end_date_meta' ),
166 ),
167 'clear_campaign_amount_donated_transient' => array(
168 'version' => '1.4.18',
169 'message' => '',
170 'prompt' => false,
171 'callback' => array( $this, 'clear_campaign_amount_donated_transient' ),
172 ),
173 'trim_upgrade_log' => array(
174 'version' => '1.4.18',
175 'message' => '',
176 'prompt' => false,
177 'callback' => array( $this, 'trim_upgrade_log' ),
178 ),
179 'remove_duplicate_donors' => array(
180 'version' => '1.5.0',
181 'message' => __( 'Charitable needs to remove duplicate donor records.', 'charitable' ),
182 'prompt' => true,
183 ),
184 'flush_permalinks_150' => array(
185 'version' => '1.5.0',
186 'message' => '',
187 'prompt' => false,
188 'callback' => array( $this, 'flush_permalinks' ),
189 ),
190 'release_notes_150' => array(
191 'version' => '1.5.0',
192 'notice' => 'release-150',
193 ),
194 'update_tables_154' => array(
195 'version' => '1.5.4',
196 'message' => '',
197 'prompt' => false,
198 'callback' => array( $this, 'update_tables' ),
199 ),
200 'fix_donor_role_caps' => array(
201 'version' => '1.5.9',
202 'message' => '',
203 'prompt' => false,
204 'callback' => array( $this, 'fix_donor_role_caps' ),
205 ),
206 'upgrade_donor_tables' => array(
207 'version' => '1.6.0',
208 'message' => __( 'Charitable needs to upgrade its database tables.', 'charitable' ),
209 'prompt' => true,
210 ),
211 'release_notes_160' => array(
212 'version' => '1.6.0',
213 'notice' => 'release-160',
214 ),
215 'fix_empty_donor_ids' => array(
216 'version' => '1.6.5',
217 'message' => __( 'Charitable needs to fix donations saved without a donor ID.', 'charitable' ),
218 'prompt' => true,
219 'active_callback' => array( $this, 'has_empty_donor_ids' ),
220 ),
221 'flush_permalinks_1614' => array(
222 'version' => '1.6.14',
223 'message' => '',
224 'prompt' => false,
225 'callback' => array( $this, 'flush_permalinks' ),
226 ),
227 'flush_permalinks_1621' => array(
228 'version' => '1.6.21',
229 'message' => '',
230 'prompt' => false,
231 'callback' => array( $this, 'flush_permalinks' ),
232 ),
233 'remove_campaign_manager_unfiltered_html_cap' => array(
234 'version' => '1.6.51',
235 'message' => '',
236 'prompt' => false,
237 'callback' => array( $this, 'remove_campaign_manager_unfiltered_html_cap' ),
238 ),
239 'create_activity_tables' => array(
240 'version' => '1.8.1',
241 'message' => __( 'Charitable needs to add additional database tables.', 'charitable' ),
242 'prompt' => true,
243 ),
244 'detect_paypal_user_tier' => array(
245 'version' => '1.8.11',
246 'message' => '',
247 'prompt' => false,
248 'callback' => array( $this, 'detect_paypal_user_tier' ),
249 ),
250 'create_logs_table' => array(
251 'version' => '1.8.11',
252 'message' => __( 'Create the logs table.', 'charitable' ),
253 'prompt' => false,
254 'callback' => array( $this, 'create_logs_table' ),
255 ),
256 'verify_logs_table' => array(
257 'version' => '1.8.11',
258 'message' => '',
259 'prompt' => false,
260 'callback' => array( $this, 'verify_logs_table' ),
261 ),
262 );
263 }
264
265 /**
266 * Use the old upgrade mode.
267 *
268 * @since 1.5.0
269 *
270 * @param false|string $db_version Current pre-update version.
271 * @param string $edge_version The version we're upgrading to.
272 * @return void
273 */
274 public function legacy_upgrade_mode( $db_version, $edge_version ) {
275 $this->db_version = $db_version;
276 $this->edge_version = $edge_version;
277
278 /* Perform version upgrades. */
279 $this->do_upgrades();
280
281 /* Log the upgrade and update the database version. */
282 $this->save_upgrade_log();
283 $this->update_db_version();
284 }
285
286 /**
287 * Populate the upgrade log when first installing the plugin.
288 *
289 * @since 1.3.0
290 *
291 * @return void
292 */
293 public function populate_upgrade_log_on_install() {
294 /**
295 * If the log already exists, don't change it.
296 */
297 if ( get_option( $this->upgrade_log_key ) ) {
298 return;
299 }
300
301 $log = array(
302 'install' => array(
303 'time' => time(),
304 'version' => Charitable::VERSION,
305 'message' => __( 'Charitable was installed.', 'charitable' ),
306 ),
307 );
308
309 $this->init();
310
311 foreach ( $this->upgrade_actions as $key => $notes ) {
312 $log[ $key ] = array(
313 'time' => time(),
314 'version' => Charitable::VERSION,
315 'install' => true,
316 );
317 }
318
319 add_option( $this->upgrade_log_key, $log );
320 }
321
322 /**
323 * Check if there is an upgrade that needs to happen and if so, displays a notice to begin upgrading.
324 *
325 * @since 1.3.0
326 *
327 * @return void
328 */
329 public function add_upgrade_notice() {
330 if ( isset( $_GET['page'] ) && 'charitable-upgrades' == $_GET['page'] ) { // phpcs:ignore
331 return;
332 }
333
334 if ( ! current_user_can( 'manage_charitable_settings' ) ) { // phpcs:ignore
335 return;
336 }
337
338 /**
339 * If an upgrade is still in progress, continue it until it's done.
340 */
341 if ( ! self::get_instance()->notice_shown_for_upgrade_in_progress() ) {
342 $upgrade_progress = $this->upgrade_is_in_progress();
343
344 if ( false !== $upgrade_progress ) {
345 return self::get_instance()->show_upgrade_in_progress_notice( $upgrade_progress );
346 }
347 }
348
349 $this->walk_upgrade_actions( array( $this, 'show_upgrade_notice' ) );
350 }
351
352 /**
353 * Show upgrade notice for a particular upgrade notice.
354 *
355 * @since 1.5.0
356 * @version 1.8.1 // added nonce.
357 *
358 * @param string $action The upgrade action key.
359 * @param array $upgrade Upgrade args.
360 * @return boolean
361 */
362 public function show_upgrade_notice( $action, $upgrade ) {
363 if ( $this->do_upgrade_immediately( $upgrade ) ) {
364 return;
365 }
366
367 /* Check if we're just setting a transient to display a notice. */
368 if ( array_key_exists( 'notice', $upgrade ) ) {
369 return $this->set_update_notice_transient( $upgrade, $action );
370 }
371
372 /* Add a nonce */
373 $action .= '&nonce=' . wp_create_nonce( 'charitable-upgrade' );
374
375 ?>
376 <div class="updated">
377 <p>
378 <?php
379 printf(
380 '%s %s',
381 $upgrade['message'], // phpcs:ignore
382 sprintf(
383 /* translators: %s: upgrade link */
384 __( 'Click <a href="%s">here</a> to start the upgrade.', 'charitable' ), // phpcs:ignore
385 esc_url( admin_url( 'index.php?page=charitable-upgrades&charitable-upgrade=' . $action ) )
386 )
387 );
388 ?>
389 </p>
390 </div>
391 <?php
392 }
393
394 /**
395 * Perform any immediate upgrades.
396 *
397 * @since 1.5.0
398 *
399 * @return void
400 */
401 public function do_immediate_upgrades() {
402 if ( is_admin() && isset( $_GET['page'] ) && 'charitable-upgrades' == $_GET['page'] ) { // phpcs:ignore
403 return;
404 }
405
406 $this->walk_upgrade_actions( array( $this, 'perform_immediate_upgrade' ) );
407 }
408
409 /**
410 * Perform an immediate upgrade and log the result.
411 *
412 * @since 1.5.0
413 *
414 * @param string $action The upgrade's action key.
415 * @param array $upgrade Upgrade details.
416 * @return void
417 */
418 public function perform_immediate_upgrade( $action, $upgrade ) {
419 if ( $this->do_upgrade_immediately( $upgrade ) ) {
420 $callback = $upgrade['callback'];
421 if ( is_callable( $callback ) ) {
422 $ret = $callback( $action );
423 } else {
424 $ret = false;
425 }
426
427 /* If the upgrade succeeded, update the log. */
428 if ( $ret ) {
429 $this->update_upgrade_log( $action );
430 }
431 }
432 }
433
434 /**
435 * Walk over the array of upgrade actions, performing the callback
436 * for any upgrades that have not been completed yet.
437 *
438 * @since 1.5.0
439 * @version 1.8.5.6
440 *
441 * @param callback $callback Callback method to perform if the
442 * upgrade has not been completed.
443 * @return void
444 */
445 protected function walk_upgrade_actions( $callback ) {
446
447 if ( ! is_callable( $callback ) ) {
448 return;
449 }
450
451 if ( $this->upgrade_log_key === 'charitable_upgrade_log' ) {
452 $this->init();
453 }
454
455 foreach ( $this->upgrade_actions as $action => $upgrade ) {
456
457 if ( ! $this->upgrade_has_been_completed( $action ) ) {
458
459 /* If the upgrade has an active_callback, check whether the upgrade is required. If not, mark it as done. */
460 $active_callback = $upgrade['active_callback'] ?? null;
461 $is_active = $active_callback && is_callable( $active_callback ) ? $active_callback() : true;
462 if ( array_key_exists( 'active_callback', $upgrade ) && ! $is_active ) {
463 $this->update_upgrade_log( $action );
464 continue;
465 }
466
467 $callback( $action, $upgrade );
468
469 }
470
471 }
472 }
473
474 /**
475 * Checks whether an upgrade has been completed.
476 *
477 * @since 1.3.0
478 * @since 1.6.0 Changed access to public.
479 *
480 * @param string $action The upgrade action.
481 * @return boolean
482 */
483 public function upgrade_has_been_completed( $action ) {
484 $log = get_option( $this->upgrade_log_key );
485 return is_array( $log ) && array_key_exists( $action, $log );
486 }
487
488
489 /**
490 * Check whether a notice has been shown to say that an upgrade is in progress.
491 *
492 * @since 1.6.14
493 *
494 * @return boolean
495 */
496 public function notice_shown_for_upgrade_in_progress() {
497 return $this->notice_shown_for_upgrade_in_progress;
498 }
499
500 /**
501 * Show the "upgrade in progress" notice.
502 *
503 * @since 1.6.14
504 *
505 * @param array $upgrade_progress Array detailing the upgrade progress.
506 * @return void
507 */
508 public function show_upgrade_in_progress_notice( $upgrade_progress ) {
509 /* Fixes a bug that incorrectly set the page as charitable-upgrade */
510 if ( isset( $upgrade_progress['page'] ) && 'charitable-upgrade' == $upgrade_progress['page'] ) { //phpcs:ignore
511 $upgrade_progress['page'] = 'charitable-upgrades';
512 }
513 ?>
514 <div class="error">
515 <p>
516 <?php
517 printf(
518 /* translators: %s: upgrade link */
519 __( 'Charitable needs to complete an upgrade that was started earlier. Click <a href="%s">here</a> to continue the upgrade.', 'charitable' ), // phpcs:ignore
520 esc_url( add_query_arg( $upgrade_progress, admin_url( 'index.php' ) ) )
521 );
522 ?>
523 </p>
524 </div>
525 <?php
526 $this->notice_shown_for_upgrade_in_progress = true;
527 }
528
529 /**
530 * Evaluates two version numbers and determines whether an upgrade is
531 * required for version A to get to version B.
532 *
533 * @since 1.0.0
534 *
535 * @param false|string $version_a Current stored version.
536 * @param string $version_b Version we are upgrading to.
537 * @return bool
538 */
539 public static function requires_upgrade( $version_a, $version_b ) {
540 return false === $version_a || version_compare( $version_a, $version_b, '<' );
541 }
542
543 /**
544 * Set up cron.
545 *
546 * @since 1.4.18
547 *
548 * @param string $action The upgrade action.
549 * @return boolean Whether the event was scheduled.
550 */
551 public function trigger_cron( $action ) {
552 return Charitable_Cron::schedule_events();
553 }
554
555 /**
556 * This just flushes the permalinks on the `init` hook.
557 *
558 * Called by 1.0.1 and 1.1.3 update scripts.
559 *
560 * @since 1.1.3
561 *
562 * @param string $action The upgrade action.
563 * @return true Will always return true.
564 */
565 public function flush_permalinks( $action = '' ) {
566 return add_action( 'init', 'flush_rewrite_rules' );
567 }
568
569 /**
570 * Upgrade to version 1.1.0.
571 *
572 * This sets up the daily scheduled event.
573 *
574 * @since 1.1.0
575 *
576 * @return boolean Whether the event was scheduled.
577 */
578 public function upgrade_1_1_0() {
579 return Charitable_Cron::schedule_events();
580 }
581
582 /**
583 * Update the upgrade system.
584 *
585 * Also updates the campaign donations table to start storing amounts as DECIMAL, instead of FLOAT.
586 *
587 * This upgrade routine was added in 1.3.0.
588 *
589 * @see https://github.com/Charitable/Charitable/issues/56
590 *
591 * @since 1.3.0
592 *
593 * @return void
594 */
595 public function update_upgrade_system() {
596 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
597 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) ); // phpcs:ignore
598 }
599
600 ignore_user_abort( true );
601
602 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore
603 @set_time_limit( 0 ); // phpcs:ignore
604 }
605
606 /**
607 * Update the campaign donations table to use DECIMAL for amounts.
608 *
609 * @see https://github.com/Charitable/Charitable/issues/56
610 */
611 $table = new Charitable_Campaign_Donations_DB();
612 $table->create_table();
613
614 $this->upgrade_logs();
615
616 $this->finish_upgrade( 'update_upgrade_system' );
617 }
618
619 /**
620 * Fix the donation dates.
621 *
622 * This upgrade routine was added in 1.3.0
623 *
624 * @see https://github.com/Charitable/Charitable/issues/58
625 *
626 * @since 1.3.0
627 *
628 * @return void
629 */
630 public function fix_donation_dates() {
631 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
632 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) ); // phpcs:ignore
633 }
634
635 ignore_user_abort( true );
636
637 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
638 @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
639 }
640
641 // phpcs:disable WordPress.Security.NonceVerification.Recommended
642 $step = isset( $_GET['step'] ) ? absint( sanitize_text_field( wp_unslash( $_GET['step'] ) ) ) : 1;
643 // phpcs:enable WordPress.Security.NonceVerification.Recommended
644 $number = 20;
645
646 $total = Charitable_Donations::count_all();
647
648 /**
649 * If there are no donations to update, go ahead and wrap it up right now.
650 */
651 if ( ! $total ) {
652 $this->finish_upgrade( 'fix_donation_dates' );
653 }
654
655 $donations = get_posts(
656 array(
657 'post_type' => Charitable::DONATION_POST_TYPE,
658 'posts_per_page' => $number,
659 'paged' => $step,
660 'post_status' => array_keys( charitable_get_valid_donation_statuses() ),
661 )
662 );
663
664 if ( count( $donations ) ) {
665
666 /**
667 * Prevent donation receipt & admin notifications from getting resent.
668 */
669 remove_action( 'save_post_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_Donation_Receipt', 'send_with_donation_id' ) );
670 remove_action( 'save_post_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_New_Donation', 'send_with_donation_id' ) );
671
672 foreach ( $donations as $donation ) {
673
674 /**
675 * Thankfully, we store the timestamp of the donation in the log,
676 * so we can use that to correct any incorrect post_date/post_date_gmt
677 * values.
678 */
679 $donation_log = get_post_meta( $donation->ID, '_donation_log', true );
680
681 if ( empty( $donation_log ) ) {
682 continue;
683 }
684
685 $time = $donation_log[0]['time'];
686
687 $date_gmt = gmdate( 'Y-m-d H:i:s', $time );
688
689 if ( $date_gmt == $donation->post_date_gmt ) {
690 continue;
691 }
692
693 $date = get_date_from_gmt( $date_gmt );
694
695 wp_update_post(
696 array(
697 'ID' => $donation->ID,
698 'post_date' => $date,
699 'post_date_gmt' => $date_gmt,
700 )
701 );
702 }//end foreach
703
704 ++$step;
705
706 $redirect = add_query_arg(
707 array(
708 'page' => 'charitable-upgrades',
709 'charitable-upgrade' => 'fix_donation_dates',
710 'step' => $step,
711 'number' => $number,
712 'total' => $total,
713 ),
714 admin_url( 'index.php' )
715 );
716
717 wp_safe_redirect( $redirect );
718
719 exit;
720 }//end if
721
722 $this->upgrade_logs();
723
724 $this->finish_upgrade( 'fix_donation_dates' );
725 }
726
727 /**
728 * Remove duplicate donor records.
729 *
730 * This upgrade routine was added in 1.5.0
731 *
732 * @see https://github.com/Charitable/Charitable/issues/409
733 *
734 * @since 1.5.0
735 *
736 * @global WPDB $wpdb
737 * @return void
738 */
739 public function remove_duplicate_donors() {
740 global $wpdb;
741
742 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
743 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) ); // phpcs:ignore
744 }
745
746 ignore_user_abort( true );
747
748 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
749 @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
750 }
751
752 // phpcs:disable WordPress.Security.NonceVerification.Recommended
753 $step = array_key_exists( 'step', $_GET ) ? absint( sanitize_text_field( wp_unslash( $_GET['step'] ) ) ) : 1;
754 // phpcs:enable WordPress.Security.NonceVerification.Recommended
755 $number = 20;
756 $subquery = "SELECT GROUP_CONCAT(donor_id, ':', user_id) AS donor, COUNT(*) AS count
757 FROM {$wpdb->prefix}charitable_donors
758 GROUP BY email";
759 $total = $wpdb->get_var( "SELECT COUNT( d.donor ) FROM ( {$subquery} ) AS d WHERE d.count > 1" ); // phpcs:ignore
760
761 /**
762 * If there are no donors left to remove, go ahead and wrap it up right now.
763 */
764 if ( ! $total ) {
765 $this->finish_upgrade( 'remove_duplicate_donors' );
766 }
767
768 $donors = $wpdb->get_col( "SELECT d.donor FROM ( {$subquery} ) AS d WHERE d.count > 1" ); // phpcs:ignore
769
770 if ( count( $donors ) ) {
771
772 foreach ( $donors as $donor ) {
773
774 $records = explode( ',', $donor );
775 $canonical = false;
776 $duplicates = array();
777
778 foreach ( $records as $record ) {
779 list( $donor_id, $user_id ) = explode( ':', $record );
780
781 if ( 0 == $user_id && false === $canonical ) {
782 $canonical = $donor_id;
783 } else {
784 $duplicates[] = $donor_id;
785 }
786 }
787
788 /**
789 * It seems very unlikely that this would ever be the case, but
790 * it could theoretically happen if someone registers an account,
791 * donates while logged in, then registers a different account
792 * (using a different email address), and makes a donation while
793 * logged into that account but with the first email address. In
794 * that case, there could be two donor records, both with user IDs.
795 *
796 * To reconcile that, we just pick the first donor record and consider
797 * the remainder as duplicates.
798 */
799 if ( false === $canonical ) {
800 $canonical = array_shift( $duplicates );
801 }
802
803 /* Update donations from duplicate accounts and set canonical donor_id. */
804 $donor_id_placeholders = charitable_get_query_placeholders( count( $duplicates ), '%d' );
805 $sql = "UPDATE {$wpdb->prefix}charitable_campaign_donations
806 SET donor_id = %d
807 WHERE donor_id IN ( {$donor_id_placeholders} )";
808
809 $parameters = array_merge( array( $canonical ), $duplicates );
810 $updated = $wpdb->query( $wpdb->prepare( $sql, array_merge( array( $canonical ), $duplicates ) ) ); // phpcs:ignore
811
812 if ( false !== $updated ) {
813 $sql = "DELETE FROM {$wpdb->prefix}charitable_donors WHERE donor_id IN ( {$donor_id_placeholders} );";
814 $deleted = $wpdb->query( $wpdb->prepare( $sql, $duplicates ) ); // phpcs:ignore
815 }
816 }//end foreach
817
818 ++$step;
819
820 $redirect = add_query_arg(
821 array(
822 'charitable-upgrade' => 'remove_duplicate_donors',
823 'page' => 'charitable-upgrades',
824 'step' => $step,
825 'number' => $number,
826 'total' => $total,
827 ),
828 admin_url( 'index.php' )
829 );
830
831 wp_safe_redirect( $redirect );
832
833 exit;
834 }//end if
835
836 $this->finish_upgrade( 'remove_duplicate_donors' );
837 }
838
839 /**
840 * Upgrade the logs structure.
841 *
842 * This upgrade routine was added in 1.3.0.
843 *
844 * @see Charitable_Upgrade::update_upgrade_system()
845 * @see Charitable_upgrade::fix_donation_dates()
846 *
847 * @since 1.3.0
848 *
849 * @return void
850 */
851 public function upgrade_logs() {
852 /**
853 * Deal with old upgrades.
854 */
855 $log = get_option( $this->upgrade_log_key, false );
856
857 /**
858 * Both of the 1.3 upgrades call this, so we need to make sure it hasn't run yet.
859 */
860 if ( is_array( $log ) && isset( $log['legacy_logs'] ) ) {
861 return;
862 }
863
864 $last_log = ! is_array( $log ) ? false : end( $log );
865
866 /**
867 * If we're upgrading from prior to 1.1.0, we'll schedule events and flush rewrite rules.
868 */
869 if ( false === $last_log || version_compare( $last_log['to'], '1.1.0', '<' ) ) {
870 Charitable_Cron::schedule_events(); // 1.1.0 upgrade
871 flush_rewrite_rules(); // 1.2.0 upgrade
872 } /**
873 * If we're upgrade from prior to 1.2.0, we'll just flush the rewrite rules.
874 */
875 elseif ( version_compare( $last_log['to'], '1.2.0', '<' ) ) {
876 flush_rewrite_rules(); // 1.2.0 upgrade
877 }
878
879 /**
880 * Update the upgrade log and save all old logs as 'legacy_logs'.
881 */
882 if ( is_array( $log ) ) {
883 $new_log = array(
884 'legacy_logs' => array(
885 'time' => time(),
886 'version' => charitable()->get_version(),
887 'logs' => $log,
888 ),
889 );
890
891 update_option( $this->upgrade_log_key, $new_log );
892 }
893 }
894
895 /**
896 * Remove the 'manage_charitable_settings' cap from the Campaign Manager role.
897 *
898 * @since 1.4.5
899 *
900 * @global WP_Roles
901 * @param string $action The upgrade action.
902 * @return true Will always return true.
903 */
904 public function remove_campaign_manager_cap( $action ) { // phpcs:ignore
905 global $wp_roles;
906
907 if ( class_exists( 'WP_Roles' ) ) {
908 if ( ! isset( $wp_roles ) ) {
909 $wp_roles = new WP_Roles(); // phpcs:ignore
910 }
911 }
912
913 if ( is_object( $wp_roles ) ) {
914 $wp_roles->remove_cap( 'campaign_manager', 'manage_charitable_settings' );
915 }
916
917 return true;
918 }
919
920 /**
921 * Convert the campaign end date meta to 0 for any campaigns where it is currently blank.
922 *
923 * @since 1.4.11
924 *
925 * @global WPDB $wpdb
926 * @param string $action The upgrade action.
927 * @return boolean Whether the query was successfully executed.
928 */
929 public function fix_empty_campaign_end_date_meta( $action ) { // phpcs:ignore
930 global $wpdb;
931
932 $sql = "UPDATE $wpdb->postmeta
933 INNER JOIN $wpdb->posts
934 ON $wpdb->posts.ID = $wpdb->postmeta.post_id
935 SET $wpdb->postmeta.meta_value = 0
936 WHERE $wpdb->postmeta.meta_key = '_campaign_end_date'
937 AND $wpdb->postmeta.meta_value = ''
938 AND $wpdb->posts.post_type = 'campaign';";
939
940 $ret = $wpdb->query( $sql ); // phpcs:ignore
941
942 return false !== $ret;
943 }
944
945 /**
946 * Clear the campaign amount donated transients.
947 *
948 * @since 1.4.18
949 *
950 * @global WPDB $wpdb
951 * @param string $action The upgrade action.
952 * @return boolean Whether the event was scheduled.
953 */
954 public function clear_campaign_amount_donated_transient( $action ) { // phpcs:ignore
955 global $wpdb;
956
957 $sql = "DELETE FROM $wpdb->options
958 WHERE option_name LIKE '_transient_charitable_campaign_%_donation_amount'";
959
960 $ret = $wpdb->query( $sql ); // phpcs:ignore
961
962 return false !== $ret;
963 }
964
965 /**
966 * Prior to 1.4.18, when first installing Charitable all the upgrade details were
967 * stored in the upgrade log (including serialized instances of any objects used).
968 * As of 1.4.18, we only store the time, version when the upgrade took place and
969 * whether the upgrade was done at install time.
970 *
971 * @since 1.4.18
972 *
973 * @return boolean Whether the log was successfully updated.
974 */
975 public function trim_upgrade_log() {
976 $log = get_option( $this->upgrade_log_key );
977 $new_log = array();
978 $keys = array( 'time', 'version', 'install' );
979
980 foreach ( $log as $action => $details ) {
981
982 $action_log = array();
983
984 if ( array_key_exists( 'time', $details ) ) {
985 $action_log['time'] = $details['time'];
986 }
987
988 if ( array_key_exists( 'version', $details ) ) {
989 $action_log['version'] = $details['version'];
990 }
991
992 if ( array_key_exists( 'install', $details ) && $details['install'] ) {
993 $action_log['install'] = $details['install'];
994
995 /* Use the version when the plugin was installed. */
996 if ( array_key_exists( 'install', $log ) ) {
997 $action_log['version'] = $log['install']['version'];
998 }
999 }
1000
1001 $new_log[ $action ] = $action_log;
1002
1003 }
1004
1005 return update_option( $this->upgrade_log_key, $new_log );
1006 }
1007
1008 /**
1009 * Set a transient to display an update notice.
1010 *
1011 * @since 1.4.0
1012 *
1013 * @param array $upgrade The upgrade details.
1014 * @param string $action The action key for the upgrade.
1015 * @return void
1016 */
1017 public function set_update_notice_transient( $upgrade, $action ) {
1018 set_transient( 'charitable_' . $upgrade['notice'] . '_notice', 1 );
1019
1020 $this->update_upgrade_log( $action );
1021 }
1022
1023 /**
1024 * Checks whether an upgrade should be completed immediately, without a prompt.
1025 *
1026 * @since 1.3.4
1027 *
1028 * @param array $upgrade The upgrade parameters.
1029 * @return boolean
1030 */
1031 protected function do_upgrade_immediately( $upgrade ) {
1032 /* If a prompt is required, return false. */
1033 if ( ! isset( $upgrade['prompt'] ) || $upgrade['prompt'] ) {
1034 return false;
1035 }
1036
1037 /* If the callback is set and it's callable, return true. */
1038 return isset( $upgrade['callback'] ) && is_callable( $upgrade['callback'] );
1039 }
1040
1041 /**
1042 * Checks whether an upgrade is in progress.
1043 *
1044 * @since 1.3.0
1045 *
1046 * @return false|array False if the upgrade is not in progress.
1047 */
1048 protected function upgrade_is_in_progress() {
1049 $doing_upgrade = get_option( 'charitable_doing_upgrade', false );
1050
1051 if ( empty( $doing_upgrade ) ) {
1052 return false;
1053 }
1054
1055 return $doing_upgrade;
1056 }
1057
1058 /**
1059 * Finish an upgrade. This clears the charitable_doing_upgrade setting and updates the log.
1060 *
1061 * @since 1.3.0
1062 * @since 1.8.1 Added redirect url for dashboard for create_activity_tables upgrade.
1063 *
1064 * @param string $upgrade The upgrade action.
1065 * @param string $redirect_url Optional URL to redirect to after the upgrade.
1066 * @return void
1067 */
1068 protected function finish_upgrade( $upgrade = '', $redirect_url = '' ) {
1069 delete_option( 'charitable_doing_upgrade' );
1070
1071 $this->update_upgrade_log( $upgrade );
1072
1073 if ( empty( $redirect_url ) ) {
1074 if ( 'create_activity_tables' === $upgrade ) {
1075 $redirect_url = admin_url( 'admin.php?page=charitable-dashboard' );
1076 } else {
1077 $redirect_url = admin_url( 'index.php' );
1078 }
1079 }
1080
1081 wp_safe_redirect( $redirect_url );
1082
1083 exit();
1084 }
1085
1086 /**
1087 * Add a completed upgrade to the upgrade log.
1088 *
1089 * @since 1.3.0
1090 *
1091 * @param string $upgrade The upgrade action.
1092 * @return boolean False if value was not updated and true if value was updated.
1093 */
1094 protected function update_upgrade_log( $upgrade ) {
1095 $log = get_option( $this->upgrade_log_key );
1096
1097 $log[ $upgrade ] = array(
1098 'time' => time(),
1099 'version' => charitable()->get_version(),
1100 );
1101
1102 return update_option( $this->upgrade_log_key, $log );
1103 }
1104
1105 /**
1106 * Update the campaign donations and donors tables.
1107 *
1108 * @since 1.5.4
1109 *
1110 * @return boolean Whether tables were successfully updated.
1111 */
1112 protected function update_tables() {
1113 try {
1114 charitable_get_table( 'campaign_donations' )->create_table();
1115 charitable_get_table( 'donors' )->create_table();
1116 return true;
1117 } catch ( Exception $e ) {
1118 return false;
1119 }
1120 }
1121
1122 /**
1123 * Update the donors table and add the donormeta table.
1124 *
1125 * @since 1.6.0
1126 *
1127 * @return boolean Whether tables were successfully updated.
1128 */
1129 public function upgrade_donor_tables() {
1130 try {
1131 charitable_get_table( 'donors' )->create_table();
1132 charitable_get_table( 'donormeta' )->create_table();
1133
1134 $this->finish_upgrade( 'upgrade_donor_tables' );
1135
1136 return true;
1137 } catch ( Exception $e ) {
1138 return false;
1139 }
1140 }
1141
1142 /**
1143 * Fix the donor role caps.
1144 *
1145 * @since 1.5.9
1146 *
1147 * @return true
1148 */
1149 public function fix_donor_role_caps() {
1150 remove_role( 'donor' );
1151
1152 add_role( 'donor', __( 'Donor', 'charitable' ), array( 'read' => true ) );
1153
1154 return true;
1155 }
1156
1157 /**
1158 * Update the unfiltered_html cap for Campaign Managers.
1159 *
1160 * @since 1.6.51
1161 *
1162 * @return true
1163 */
1164 public function remove_campaign_manager_unfiltered_html_cap() {
1165 global $wp_roles;
1166
1167 if ( class_exists( 'WP_Roles' ) ) {
1168 if ( ! isset( $wp_roles ) ) {
1169 $wp_roles = new WP_Roles(); // phpcs:ignore
1170 }
1171 }
1172
1173 if ( is_object( $wp_roles ) ) {
1174 $wp_roles->remove_cap( 'campaign_manager', 'unfiltered_html' );
1175 }
1176
1177 return true;
1178 }
1179
1180 /**
1181 * Checks whether the site has donations missing a donor id.
1182 *
1183 * @since 1.6.5
1184 *
1185 * @return boolean
1186 */
1187 protected function has_empty_donor_ids() {
1188 return 0 < $this->count_empty_donor_id_donations( get_option( 'charitable_skipped_donations_with_empty_donor_id', array() ) );
1189 }
1190
1191 /**
1192 * Count donations with empty donor ids.
1193 *
1194 * @since 1.6.6
1195 *
1196 * @param array $skipped List of skipped donations.
1197 * @return int
1198 */
1199 protected function count_empty_donor_id_donations( $skipped = array() ) {
1200 if ( empty( $skipped ) ) {
1201 return charitable()->get_db_table( 'campaign_donations' )->count_donations_by_donor( 0, true );
1202 }
1203
1204 global $wpdb;
1205
1206 $placeholders = charitable_get_query_placeholders( count( $skipped ), '%d' );
1207 $sql = "SELECT COUNT(*)
1208 FROM {$wpdb->prefix}charitable_campaign_donations
1209 WHERE donor_id = 0
1210 AND donation_id NOT IN ( $placeholders )";
1211
1212 return $wpdb->get_var( $wpdb->prepare( $sql, $skipped ) ); // phpcs:ignore
1213 }
1214
1215 /**
1216 * Get donations with empty donor ids.
1217 *
1218 * @since 1.6.6
1219 *
1220 * @param array $skipped List of skipped donations.
1221 * @param int $number The number of donations to retrieve.
1222 * @return int
1223 */
1224 protected function get_empty_donor_id_donations( $skipped = array(), $number = 20 ) {
1225 global $wpdb;
1226
1227 if ( empty( $skipped ) ) {
1228 return $wpdb->get_col( // phpcs:ignore
1229 "SELECT DISTINCT donation_id
1230 FROM {$wpdb->prefix}charitable_campaign_donations
1231 WHERE donor_id = 0
1232 LIMIT $number;" // phpcs:ignore
1233 );
1234 }
1235
1236 $placeholders = charitable_get_query_placeholders( count( $skipped ), '%d' );
1237
1238 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1239 // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $placeholders is built by charitable_get_query_placeholders() which returns safe placeholder strings like '%d, %d, %d'. Values are passed to prepare().
1240 return $wpdb->get_col(
1241 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $placeholders is dynamically built and sanitized
1242 $wpdb->prepare(
1243 "SELECT DISTINCT donation_id
1244 FROM {$wpdb->prefix}charitable_campaign_donations
1245 WHERE donor_id = 0
1246 AND donation_id NOT IN ( $placeholders )
1247 LIMIT %d;",
1248 ...array_merge( $skipped, array( $number ) )
1249 )
1250 );
1251 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
1252 }
1253
1254 /**
1255 * Fix donations with missing donor ids.
1256 *
1257 * @since 1.6.5
1258 *
1259 * @return void
1260 */
1261 public function fix_empty_donor_ids() {
1262 global $wpdb;
1263
1264 if ( ! current_user_can( 'manage_charitable_settings' ) ) { // phpcs:ignore
1265 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) ); // phpcs:ignore
1266 }
1267
1268 ignore_user_abort( true );
1269
1270 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
1271 @set_time_limit( 0 ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
1272 }
1273
1274 // phpcs:disable WordPress.Security.NonceVerification.Recommended
1275 $step = array_key_exists( 'step', $_GET ) ? absint( sanitize_text_field( wp_unslash( $_GET['step'] ) ) ) : 1;
1276 // phpcs:enable WordPress.Security.NonceVerification.Recommended
1277 $number = 20;
1278 $skipped = get_option( 'charitable_skipped_donations_with_empty_donor_id', array() );
1279 $total = $this->count_empty_donor_id_donations( $skipped );
1280
1281 /**
1282 * If there are no donors left to remove, go ahead and wrap it up right now.
1283 */
1284 if ( ! $total ) {
1285 delete_option( 'charitable_skipped_donations_with_empty_donor_id' );
1286
1287 $this->finish_upgrade( 'fix_empty_donor_ids' );
1288 }
1289
1290 $donations_table = charitable()->get_db_table( 'campaign_donations' );
1291 $donors_table = charitable()->get_db_table( 'donors' );
1292 $donations = $this->get_empty_donor_id_donations( $skipped, $number );
1293
1294 if ( count( $donations ) ) {
1295
1296 foreach ( $donations as $donation_id ) {
1297 $data = get_post_meta( $donation_id, 'donor', true );
1298
1299 if ( ! array_key_exists( 'email', $data ) || empty( $data['email'] ) ) {
1300 $skipped[] = $donation_id;
1301 continue;
1302 }
1303
1304 $donor_id = $donors_table->get_donor_id_by_email( $data['email'] );
1305
1306 if ( ! $donor_id ) {
1307 $donor_id = $donors_table->insert(
1308 array(
1309 'email' => $data['email'],
1310 'first_name' => array_key_exists( 'first_name', $data ) ? $data['first_name'] : '',
1311 'last_name' => array_key_exists( 'last_name', $data ) ? $data['last_name'] : '',
1312 'date_joined' => get_post_field( 'post_date_gmt', $donation_id ),
1313 )
1314 );
1315 }
1316
1317 if ( $donor_id ) {
1318 $donations_table->update(
1319 $donation_id,
1320 array(
1321 'donor_id' => $donor_id,
1322 ),
1323 'donation_id'
1324 );
1325 } else {
1326 $skipped[] = $donation_id;
1327 }
1328 }//end foreach
1329
1330 update_option( 'charitable_skipped_donations_with_empty_donor_id', $skipped );
1331
1332 ++$step;
1333
1334 $redirect = add_query_arg(
1335 array(
1336 'charitable-upgrade' => 'fix_empty_donor_ids',
1337 'page' => 'charitable-upgrades',
1338 'step' => $step,
1339 'number' => $number,
1340 'total' => $total,
1341 ),
1342 admin_url( 'index.php' )
1343 );
1344
1345 wp_safe_redirect( $redirect );
1346
1347 exit;
1348 }//end if
1349
1350 delete_option( 'charitable_skipped_donations_with_empty_donor_id' );
1351
1352 $this->finish_upgrade( 'fix_empty_donor_ids' );
1353 }
1354
1355 /**
1356 * Create donation and campaign activity tables.
1357 *
1358 * @since 1.8.1
1359 *
1360 * @return boolean Whether tables were successfully updated.
1361 */
1362 public function create_activity_tables() {
1363 try {
1364
1365 // test and verify nonce in the $_GET array.
1366 if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( $_GET['nonce'], 'charitable-upgrade' ) ) { // phpcs:ignore
1367 return false;
1368 }
1369
1370 charitable_get_table( 'donation_activities' )->create_table();
1371 charitable_get_table( 'campaign_activities' )->create_table();
1372
1373 $this->finish_upgrade( 'create_activity_tables' );
1374
1375 return true;
1376 } catch ( Exception $e ) {
1377 return false;
1378 }
1379 }
1380
1381 /**
1382 * Create the charitable_logs table.
1383 *
1384 * Returns true so perform_immediate_upgrade() logs completion. We
1385 * deliberately do NOT call finish_upgrade() here because it forces a
1386 * wp_safe_redirect to the dashboard, which would interrupt whatever
1387 * admin page the user was loading on the init-priority-5 firing.
1388 *
1389 * @since 1.8.11
1390 *
1391 * @return bool
1392 */
1393 public function create_logs_table() {
1394 $db = new Charitable_Log_DB();
1395 $db->create_table();
1396
1397 return true;
1398 }
1399
1400 /**
1401 * Defensive follow-up to create_logs_table for fresh installs where
1402 * the upgrade flag was pre-marked but the table was never created.
1403 *
1404 * Same no-finish_upgrade rationale as create_logs_table.
1405 *
1406 * @since 1.8.11
1407 *
1408 * @return bool
1409 */
1410 public function verify_logs_table() {
1411 $db = new Charitable_Log_DB();
1412
1413 if ( ! $db->table_exists() ) {
1414 $db->create_table();
1415 }
1416
1417 return true;
1418 }
1419
1420 /**
1421 * HERE BE DEPRECATED FUNCTIONS...
1422 *
1423 * We're keeping these functions since Charitable add-ons extend this
1424 * class and don't know whether those have upgraded.
1425 */
1426
1427 /**
1428 * Upgrade from the current version stored in the database to the live version.
1429 *
1430 * @since 1.0.0
1431 *
1432 * @param false|string $db_version The version stored in the database.
1433 * @param string $edge_version The new version to upgrade to.
1434 * @return void
1435 */
1436 public static function upgrade_from( $db_version, $edge_version ) {
1437 if ( self::requires_upgrade( $db_version, $edge_version ) ) {
1438 new Charitable_Upgrade( $db_version, $edge_version );
1439 }
1440 }
1441
1442 /**
1443 * Perform version upgrades.
1444 *
1445 * @since 1.0.0
1446 *
1447 * @return void
1448 */
1449 protected function do_upgrades() {
1450 /**
1451 * Before Charitable 1.3, upgrades were in a simple key=>value
1452 * array format.
1453 *
1454 * $upgrade_actions = array(
1455 * '1.0.1' => 'flush_permalinks',
1456 * '1.1.0' => 'upgrade_1_1_0',
1457 * '1.1.3' => 'flush_permalinks',
1458 * '1.2.0' => 'flush_permalinks',
1459 * );
1460 */
1461
1462 $this->init();
1463
1464 if ( empty( $this->upgrade_actions ) || ! is_array( $this->upgrade_actions ) ) {
1465 return;
1466 }
1467
1468 foreach ( $this->upgrade_actions as $version => $method ) {
1469
1470 /**
1471 * The do_upgrades method was called, but the $upgrade_actions data structure has changed.
1472 *
1473 * This should never happen.
1474 */
1475 if ( is_array( $method ) ) {
1476 return;
1477 }
1478
1479 if ( self::requires_upgrade( $this->db_version, $version ) ) {
1480
1481 if ( method_exists( $this, $method ) ) {
1482 $this->$method();
1483 }
1484
1485 }
1486 }
1487 }
1488
1489 /**
1490 * Saves a log of the version to version upgrades made.
1491 *
1492 * @since 1.0.0
1493 *
1494 * @return void
1495 */
1496 protected function save_upgrade_log() {
1497 $log = get_option( $this->upgrade_log_key );
1498
1499 if ( false === $log || ! is_array( $log ) ) {
1500 $log = array();
1501 }
1502
1503 $log[] = array(
1504 'timestamp' => time(),
1505 'from' => $this->db_version,
1506 'to' => $this->edge_version,
1507 );
1508
1509 update_option( $this->upgrade_log_key, $log );
1510 }
1511
1512 /**
1513 * Upgrade complete. This saves the new version to the database.
1514 *
1515 * @since 1.0.0
1516 *
1517 * @return void
1518 */
1519 protected function update_db_version() {
1520 update_option( $this->version_key, $this->edge_version );
1521 }
1522
1523 /**
1524 * Get the upgrade log key.
1525 *
1526 * @since 1.8.1
1527 *
1528 * @return string
1529 */
1530 public function get_upgrade_log_key() {
1531 return $this->upgrade_log_key;
1532 }
1533
1534 /**
1535 * Detect and lock the PayPal user tier on first 1.8.11 upgrade.
1536 *
1537 * Runs once at the 1.8.11 upgrade. Writes the option `charitable_paypal_user_tier`
1538 * with value `legacy` or `commerce`. The value is permanent — future donations,
1539 * gateway state changes, or imports do NOT re-tag the site.
1540 *
1541 * Tagging rule:
1542 * - `legacy` → legacy `paypal` gateway is currently active OR
1543 * any donation/recurring-donation post has
1544 * `donation_gateway = 'paypal'` postmeta.
1545 * - `commerce` → otherwise.
1546 *
1547 * The result is escapable at runtime via the `pre_option_charitable_paypal_user_tier`
1548 * filter or by direct UPDATE on `wp_options`.
1549 *
1550 * @since 1.8.11
1551 *
1552 * @return boolean True after the upgrade is recorded.
1553 */
1554 public function detect_paypal_user_tier() {
1555 $existing = get_option( 'charitable_paypal_user_tier', null );
1556
1557 if ( null === $existing ) {
1558 $tier = 'commerce';
1559
1560 // Read active_gateways directly from charitable_settings — bypassing
1561 // `Charitable_Gateways::is_active_gateway()` because that path runs
1562 // `apply_paypal_tier_visibility()`, which strips `paypal` from the
1563 // active set when the tier option is unset (default `commerce`).
1564 // Without this direct read, the active-gateway detection branch
1565 // would never fire, defeating the primary detection signal for
1566 // sites with the legacy gateway active but no donation history.
1567 $settings = get_option( 'charitable_settings', array() );
1568 $active_gateways = ( ! empty( $settings['active_gateways'] ) && is_array( $settings['active_gateways'] ) )
1569 ? $settings['active_gateways']
1570 : array();
1571
1572 if ( isset( $active_gateways['paypal'] ) ) {
1573 $tier = 'legacy';
1574 } else {
1575 global $wpdb;
1576
1577 $has_legacy_history = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
1578 "SELECT 1 FROM {$wpdb->postmeta} pm
1579 JOIN {$wpdb->posts} p ON pm.post_id = p.ID
1580 WHERE pm.meta_key = 'donation_gateway'
1581 AND pm.meta_value = 'paypal'
1582 AND p.post_type IN ( 'donation', 'recurring_donation' )
1583 LIMIT 1"
1584 );
1585
1586 if ( $has_legacy_history ) {
1587 $tier = 'legacy';
1588 }
1589 }
1590
1591 add_option( 'charitable_paypal_user_tier', $tier, '', 'yes' );
1592 }
1593
1594 $this->finish_upgrade( 'detect_paypal_user_tier' );
1595
1596 return true;
1597 }
1598 }
1599
1600 endif;
1601