PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13
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 1.7.0.9 All 211 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.13, at includes/upgrades/class-charitable-upgrade.php

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