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

1,362 lines 35.8 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) 2022, 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 $this->upgrade_actions = array(
124 'update_upgrade_system' => array(
125 'version' => '1.3.0',
126 'message' => __( 'Charitable needs to update the database.', 'charitable' ),
127 'prompt' => true,
128 ),
129 'fix_donation_dates' => array(
130 'version' => '1.3.0',
131 'message' => __( 'Charitable needs to fix incorrect donation dates.', 'charitable' ),
132 'prompt' => true,
133 ),
134 'trigger_cron' => array(
135 'version' => '1.3.4',
136 'message' => '',
137 'prompt' => false,
138 'callback' => array( $this, 'trigger_cron' ),
139 ),
140 'flush_permalinks_140' => array(
141 'version' => '1.4.0',
142 'message' => '',
143 'prompt' => false,
144 'callback' => array( $this, 'flush_permalinks' ),
145 ),
146 'remove_campaign_manager_cap' => array(
147 'version' => '1.4.5',
148 'message' => '',
149 'prompt' => false,
150 'callback' => array( $this, 'remove_campaign_manager_cap' ),
151 ),
152 'fix_empty_campaign_end_date_meta' => array(
153 'version' => '1.4.11',
154 'message' => '',
155 'prompt' => false,
156 'callback' => array( $this, 'fix_empty_campaign_end_date_meta' ),
157 ),
158 'clear_campaign_amount_donated_transient' => array(
159 'version' => '1.4.18',
160 'message' => '',
161 'prompt' => false,
162 'callback' => array( $this, 'clear_campaign_amount_donated_transient' ),
163 ),
164 'trim_upgrade_log' => array(
165 'version' => '1.4.18',
166 'message' => '',
167 'prompt' => false,
168 'callback' => array( $this, 'trim_upgrade_log' ),
169 ),
170 'remove_duplicate_donors' => array(
171 'version' => '1.5.0',
172 'message' => __( 'Charitable needs to remove duplicate donor records.', 'charitable' ),
173 'prompt' => true,
174 ),
175 'flush_permalinks_150' => array(
176 'version' => '1.5.0',
177 'message' => '',
178 'prompt' => false,
179 'callback' => array( $this, 'flush_permalinks' ),
180 ),
181 'release_notes_150' => array(
182 'version' => '1.5.0',
183 'notice' => 'release-150',
184 ),
185 'update_tables_154' => array(
186 'version' => '1.5.4',
187 'message' => '',
188 'prompt' => false,
189 'callback' => array( $this, 'update_tables' ),
190 ),
191 'fix_donor_role_caps' => array(
192 'version' => '1.5.9',
193 'message' => '',
194 'prompt' => false,
195 'callback' => array( $this, 'fix_donor_role_caps' ),
196 ),
197 'upgrade_donor_tables' => array(
198 'version' => '1.6.0',
199 'message' => __( 'Charitable needs to upgrade its database tables.', 'charitable' ),
200 'prompt' => true,
201 ),
202 'release_notes_160' => array(
203 'version' => '1.6.0',
204 'notice' => 'release-160',
205 ),
206 'fix_empty_donor_ids' => array(
207 'version' => '1.6.5',
208 'message' => __( 'Charitable needs to fix donations saved without a donor ID.', 'charitable' ),
209 'prompt' => true,
210 'active_callback' => array( $this, 'has_empty_donor_ids' ),
211 ),
212 'flush_permalinks_1614' => array(
213 'version' => '1.6.14',
214 'message' => '',
215 'prompt' => false,
216 'callback' => array( $this, 'flush_permalinks' ),
217 ),
218 'flush_permalinks_1621' => array(
219 'version' => '1.6.21',
220 'message' => '',
221 'prompt' => false,
222 'callback' => array( $this, 'flush_permalinks' ),
223 ),
224 'remove_campaign_manager_unfiltered_html_cap' => array(
225 'version' => '1.6.51',
226 'message' => '',
227 'prompt' => false,
228 'callback' => array( $this, 'remove_campaign_manager_unfiltered_html_cap' ),
229 ),
230 );
231 }
232
233 /**
234 * Use the old upgrade mode.
235 *
236 * @since 1.5.0
237 *
238 * @param false|string $db_version Current pre-update version.
239 * @param string $edge_version The version we're upgrading to.
240 * @return void
241 */
242 public function legacy_upgrade_mode( $db_version, $edge_version ) {
243 $this->db_version = $db_version;
244 $this->edge_version = $edge_version;
245
246 /* Perform version upgrades. */
247 $this->do_upgrades();
248
249 /* Log the upgrade and update the database version. */
250 $this->save_upgrade_log();
251 $this->update_db_version();
252 }
253
254 /**
255 * Populate the upgrade log when first installing the plugin.
256 *
257 * @since 1.3.0
258 *
259 * @return void
260 */
261 public function populate_upgrade_log_on_install() {
262 /**
263 * If the log already exists, don't change it.
264 */
265 if ( get_option( $this->upgrade_log_key ) ) {
266 return;
267 }
268
269 $log = array(
270 'install' => array(
271 'time' => time(),
272 'version' => Charitable::VERSION,
273 'message' => __( 'Charitable was installed.', 'charitable' ),
274 ),
275 );
276
277 foreach ( $this->upgrade_actions as $key => $notes ) {
278 $log[ $key ] = array(
279 'time' => time(),
280 'version' => Charitable::VERSION,
281 'install' => true,
282 );
283 }
284
285 add_option( $this->upgrade_log_key, $log );
286 }
287
288 /**
289 * Check if there is an upgrade that needs to happen and if so, displays a notice to begin upgrading.
290 *
291 * @since 1.3.0
292 *
293 * @return void
294 */
295 public function add_upgrade_notice() {
296 if ( isset( $_GET['page'] ) && 'charitable-upgrades' == $_GET['page'] ) {
297 return;
298 }
299
300 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
301 return;
302 }
303
304 /**
305 * If an upgrade is still in progress, continue it until it's done.
306 */
307 if ( ! Charitable_Upgrade::get_instance()->notice_shown_for_upgrade_in_progress() ) {
308 $upgrade_progress = $this->upgrade_is_in_progress();
309
310 if ( false !== $upgrade_progress ) {
311 return Charitable_Upgrade::get_instance()->show_upgrade_in_progress_notice( $upgrade_progress );
312 }
313 }
314
315 $this->walk_upgrade_actions( array( $this, 'show_upgrade_notice' ) );
316 }
317
318 /**
319 * Show upgrade notice for a particular upgrade notice.
320 *
321 * @since 1.5.0
322 *
323 * @param string $action The upgrade action key.
324 * @param array $upgrade Upgrade args.
325 * @return boolean
326 */
327 public function show_upgrade_notice( $action, $upgrade ) {
328 if ( $this->do_upgrade_immediately( $upgrade ) ) {
329 return;
330 }
331
332 /* Check if we're just setting a transient to display a notice. */
333 if ( array_key_exists( 'notice', $upgrade ) ) {
334 return $this->set_update_notice_transient( $upgrade, $action );
335 }
336 ?>
337 <div class="updated">
338 <p>
339 <?php
340 printf(
341 '%s %s',
342 $upgrade['message'],
343 sprintf(
344 /* translators: %s: upgrade link */
345 __( 'Click <a href="%s">here</a> to start the upgrade.', 'charitable' ),
346 esc_url( admin_url( 'index.php?page=charitable-upgrades&charitable-upgrade=' . $action ) )
347 )
348 );
349 ?>
350 </p>
351 </div>
352 <?php
353 }
354
355 /**
356 * Perform any immediate upgrades.
357 *
358 * @since 1.5.0
359 *
360 * @return void
361 */
362 public function do_immediate_upgrades() {
363 if ( is_admin() && isset( $_GET['page'] ) && 'charitable-upgrades' == $_GET['page'] ) {
364 return;
365 }
366
367 $this->walk_upgrade_actions( array( $this, 'perform_immediate_upgrade' ) );
368 }
369
370 /**
371 * Perform an immediate upgrade and log the result.
372 *
373 * @since 1.5.0
374 *
375 * @param string $action The upgrade's action key.
376 * @param array $upgrade Upgrade details.
377 * @return void
378 */
379 public function perform_immediate_upgrade( $action, $upgrade ) {
380 if ( $this->do_upgrade_immediately( $upgrade ) ) {
381 $ret = call_user_func( $upgrade['callback'], $action );
382
383 /* If the upgrade succeeded, update the log. */
384 if ( $ret ) {
385 $this->update_upgrade_log( $action );
386 }
387 }
388 }
389
390 /**
391 * Walk over the array of upgrade actions, performing the callback
392 * for any upgrades that have not been completed yet.
393 *
394 * @since 1.5.0
395 *
396 * @param callback $callback Callback method to perform if the
397 * upgrade has not been completed.
398 * @return void
399 */
400 protected function walk_upgrade_actions( $callback ) {
401 if ( ! is_callable( $callback ) ) {
402 return;
403 }
404
405 foreach ( $this->upgrade_actions as $action => $upgrade ) {
406
407 if ( ! $this->upgrade_has_been_completed( $action ) ) {
408
409 /* If the upgrade has an active_callback, check whether the upgrade is required. If not, mark it as done. */
410 if ( array_key_exists( 'active_callback', $upgrade ) && ! call_user_func( $upgrade['active_callback'] ) ) {
411 $this->update_upgrade_log( $action );
412 continue;
413 }
414
415 call_user_func( $callback, $action, $upgrade );
416 }
417 }
418 }
419
420 /**
421 * Checks whether an upgrade has been completed.
422 *
423 * @since 1.3.0
424 * @since 1.6.0 Changed access to public.
425 *
426 * @param string $action The upgrade action.
427 * @return boolean
428 */
429 public function upgrade_has_been_completed( $action ) {
430 $log = get_option( $this->upgrade_log_key );
431
432 return is_array( $log ) && array_key_exists( $action, $log );
433 }
434
435
436 /**
437 * Check whether a notice has been shown to say that an upgrade is in progress.
438 *
439 * @since 1.6.14
440 *
441 * @return boolean
442 */
443 public function notice_shown_for_upgrade_in_progress() {
444 return $this->notice_shown_for_upgrade_in_progress;
445 }
446
447 /**
448 * Show the "upgrade in progress" notice.
449 *
450 * @since 1.6.14
451 *
452 * @param array $upgrade_progress Array detailing the upgrade progress.
453 * @return void
454 */
455 public function show_upgrade_in_progress_notice( $upgrade_progress ) {
456 /* Fixes a bug that incorrectly set the page as charitable-upgrade */
457 if ( isset( $upgrade_progress['page'] ) && 'charitable-upgrade' == $upgrade_progress['page'] ) {
458 $upgrade_progress['page'] = 'charitable-upgrades';
459 }
460 ?>
461 <div class="error">
462 <p>
463 <?php
464 printf(
465 /* translators: %s: upgrade link */
466 __( 'Charitable needs to complete an upgrade that was started earlier. Click <a href="%s">here</a> to continue the upgrade.', 'charitable' ),
467 esc_url( add_query_arg( $upgrade_progress, admin_url( 'index.php' ) ) )
468 );
469 ?>
470 </p>
471 </div>
472 <?php
473 $this->notice_shown_for_upgrade_in_progress = true;
474 }
475
476 /**
477 * Evaluates two version numbers and determines whether an upgrade is
478 * required for version A to get to version B.
479 *
480 * @since 1.0.0
481 *
482 * @param false|string $version_a Current stored version.
483 * @param string $version_b Version we are upgrading to.
484 * @return bool
485 */
486 public static function requires_upgrade( $version_a, $version_b ) {
487 return false === $version_a || version_compare( $version_a, $version_b, '<' );
488 }
489
490 /**
491 * Set up cron.
492 *
493 * @since 1.4.18
494 *
495 * @param string $action The upgrade action.
496 * @return boolean Whether the event was scheduled.
497 */
498 public function trigger_cron( $action ) {
499 return Charitable_Cron::schedule_events();
500 }
501
502 /**
503 * This just flushes the permalinks on the `init` hook.
504 *
505 * Called by 1.0.1 and 1.1.3 update scripts.
506 *
507 * @since 1.1.3
508 *
509 * @param string $action The upgrade action.
510 * @return true Will always return true.
511 */
512 public function flush_permalinks( $action = '' ) {
513 return add_action( 'init', 'flush_rewrite_rules' );
514 }
515
516 /**
517 * Upgrade to version 1.1.0.
518 *
519 * This sets up the daily scheduled event.
520 *
521 * @since 1.1.0
522 *
523 * @return boolean Whether the event was scheduled.
524 */
525 public function upgrade_1_1_0() {
526 return Charitable_Cron::schedule_events();
527 }
528
529 /**
530 * Update the upgrade system.
531 *
532 * Also updates the campaign donations table to start storing amounts as DECIMAL, instead of FLOAT.
533 *
534 * This upgrade routine was added in 1.3.0.
535 *
536 * @see https://github.com/Charitable/Charitable/issues/56
537 *
538 * @since 1.3.0
539 *
540 * @return void
541 */
542 public function update_upgrade_system() {
543 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
544 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) );
545 }
546
547 ignore_user_abort( true );
548
549 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
550 @set_time_limit( 0 );
551 }
552
553 /**
554 * Update the campaign donations table to use DECIMAL for amounts.
555 *
556 * @see https://github.com/Charitable/Charitable/issues/56
557 */
558 $table = new Charitable_Campaign_Donations_DB();
559 $table->create_table();
560
561 $this->upgrade_logs();
562
563 $this->finish_upgrade( 'update_upgrade_system' );
564 }
565
566 /**
567 * Fix the donation dates.
568 *
569 * This upgrade routine was added in 1.3.0
570 *
571 * @see https://github.com/Charitable/Charitable/issues/58
572 *
573 * @since 1.3.0
574 *
575 * @return void
576 */
577 public function fix_donation_dates() {
578 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
579 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) );
580 }
581
582 ignore_user_abort( true );
583
584 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
585 @set_time_limit( 0 );
586 }
587
588 $step = isset( $_GET['step'] ) ? absint( $_GET['step'] ) : 1;
589 $number = 20;
590
591 $total = Charitable_Donations::count_all();
592
593 /**
594 * If there are no donations to update, go ahead and wrap it up right now.
595 */
596 if ( ! $total ) {
597 $this->finish_upgrade( 'fix_donation_dates' );
598 }
599
600 $donations = get_posts( array(
601 'post_type' => Charitable::DONATION_POST_TYPE,
602 'posts_per_page' => $number,
603 'paged' => $step,
604 'post_status' => array_keys( charitable_get_valid_donation_statuses() ),
605 ) );
606
607 if ( count( $donations ) ) {
608
609 /**
610 * Prevent donation receipt & admin notifications from getting resent.
611 */
612 remove_action( 'save_post_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_Donation_Receipt', 'send_with_donation_id' ) );
613 remove_action( 'save_post_' . Charitable::DONATION_POST_TYPE, array( 'Charitable_Email_New_Donation', 'send_with_donation_id' ) );
614
615 foreach ( $donations as $donation ) {
616
617 /**
618 * Thankfully, we store the timestamp of the donation in the log,
619 * so we can use that to correct any incorrect post_date/post_date_gmt
620 * values.
621 */
622 $donation_log = get_post_meta( $donation->ID, '_donation_log', true );
623
624 if ( empty( $donation_log ) ) {
625 continue;
626 }
627
628 $time = $donation_log[0]['time'];
629
630 $date_gmt = gmdate( 'Y-m-d H:i:s', $time );
631
632 if ( $date_gmt == $donation->post_date_gmt ) {
633 continue;
634 }
635
636 $date = get_date_from_gmt( $date_gmt );
637
638 wp_update_post( array(
639 'ID' => $donation->ID,
640 'post_date' => $date,
641 'post_date_gmt' => $date_gmt,
642 ) );
643 }//end foreach
644
645 $step++;
646
647 $redirect = add_query_arg( array(
648 'page' => 'charitable-upgrades',
649 'charitable-upgrade' => 'fix_donation_dates',
650 'step' => $step,
651 'number' => $number,
652 'total' => $total,
653 ), admin_url( 'index.php' ) );
654
655 wp_redirect( $redirect );
656
657 exit;
658 }//end if
659
660 $this->upgrade_logs();
661
662 $this->finish_upgrade( 'fix_donation_dates' );
663 }
664
665 /**
666 * Remove duplicate donor records.
667 *
668 * This upgrade routine was added in 1.5.0
669 *
670 * @see https://github.com/Charitable/Charitable/issues/409
671 *
672 * @since 1.5.0
673 *
674 * @global WPDB $wpdb
675 * @return void
676 */
677 public function remove_duplicate_donors() {
678 global $wpdb;
679
680 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
681 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) );
682 }
683
684 ignore_user_abort( true );
685
686 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
687 @set_time_limit( 0 );
688 }
689
690 $step = array_key_exists( 'step', $_GET ) ? absint( $_GET['step'] ) : 1;
691 $number = 20;
692 $subquery = "SELECT GROUP_CONCAT(donor_id, ':', user_id) AS donor, COUNT(*) AS count
693 FROM {$wpdb->prefix}charitable_donors
694 GROUP BY email";
695 $total = $wpdb->get_var( "SELECT COUNT( d.donor ) FROM ( {$subquery} ) AS d WHERE d.count > 1" );
696
697 /**
698 * If there are no donors left to remove, go ahead and wrap it up right now.
699 */
700 if ( ! $total ) {
701 $this->finish_upgrade( 'remove_duplicate_donors' );
702 }
703
704 $donors = $wpdb->get_col( "SELECT d.donor FROM ( {$subquery} ) AS d WHERE d.count > 1" );
705
706 if ( count( $donors ) ) {
707
708 foreach ( $donors as $donor ) {
709
710 $records = explode( ',', $donor );
711 $canonical = false;
712 $duplicates = array();
713
714 foreach ( $records as $record ) {
715 list( $donor_id, $user_id ) = explode( ':', $record );
716
717 if ( 0 == $user_id && false === $canonical ) {
718 $canonical = $donor_id;
719 } else {
720 $duplicates[] = $donor_id;
721 }
722 }
723
724 /**
725 * It seems very unlikely that this would ever be the case, but
726 * it could theoretically happen if someone registers an account,
727 * donates while logged in, then registers a different account
728 * (using a different email address), and makes a donation while
729 * logged into that account but with the first email address. In
730 * that case, there could be two donor records, both with user IDs.
731 *
732 * To reconcile that, we just pick the first donor record and consider
733 * the remainder as duplicates.
734 */
735 if ( false === $canonical ) {
736 $canonical = array_shift( $duplicates );
737 }
738
739 /* Update donations from duplicate accounts and set canonical donor_id. */
740 $donor_id_placeholders = charitable_get_query_placeholders( count( $duplicates ), '%d' );
741 $sql = "UPDATE {$wpdb->prefix}charitable_campaign_donations
742 SET donor_id = %d
743 WHERE donor_id IN ( {$donor_id_placeholders} )";
744
745 $parameters = array_merge( array( $canonical ), $duplicates );
746 $updated = $wpdb->query( $wpdb->prepare( $sql, array_merge( array( $canonical ), $duplicates ) ) );
747
748 if ( false !== $updated ) {
749 $sql = "DELETE FROM {$wpdb->prefix}charitable_donors WHERE donor_id IN ( {$donor_id_placeholders} );";
750 $deleted = $wpdb->query( $wpdb->prepare( $sql, $duplicates ) );
751 }
752 }//end foreach
753
754 $step++;
755
756 $redirect = add_query_arg( array(
757 'charitable-upgrade' => 'remove_duplicate_donors',
758 'page' => 'charitable-upgrades',
759 'step' => $step,
760 'number' => $number,
761 'total' => $total,
762 ), admin_url( 'index.php' ) );
763
764 wp_redirect( $redirect );
765
766 exit;
767 }//end if
768
769 $this->finish_upgrade( 'remove_duplicate_donors' );
770 }
771
772 /**
773 * Upgrade the logs structure.
774 *
775 * This upgrade routine was added in 1.3.0.
776 *
777 * @see Charitable_Upgrade::update_upgrade_system()
778 * @see Charitable_upgrade::fix_donation_dates()
779 *
780 * @since 1.3.0
781 *
782 * @return void
783 */
784 public function upgrade_logs() {
785 /**
786 * Deal with old upgrades.
787 */
788 $log = get_option( $this->upgrade_log_key, false );
789
790 /**
791 * Both of the 1.3 upgrades call this, so we need to make sure it hasn't run yet.
792 */
793 if ( is_array( $log ) && isset( $log['legacy_logs'] ) ) {
794 return;
795 }
796
797 $last_log = ! is_array( $log ) ? false : end( $log );
798
799 /**
800 * If we're upgrading from prior to 1.1.0, we'll schedule events and flush rewrite rules.
801 */
802 if ( false === $last_log || version_compare( $last_log['to'], '1.1.0', '<' ) ) {
803 Charitable_Cron::schedule_events(); // 1.1.0 upgrade
804 flush_rewrite_rules(); // 1.2.0 upgrade
805 } /**
806 * If we're upgrade from prior to 1.2.0, we'll just flush the rewrite rules.
807 */
808 elseif ( version_compare( $last_log['to'], '1.2.0', '<' ) ) {
809 flush_rewrite_rules(); // 1.2.0 upgrade
810 }
811
812 /**
813 * Update the upgrade log and save all old logs as 'legacy_logs'.
814 */
815 if ( is_array( $log ) ) {
816 $new_log = array(
817 'legacy_logs' => array(
818 'time' => time(),
819 'version' => charitable()->get_version(),
820 'logs' => $log,
821 ),
822 );
823
824 update_option( $this->upgrade_log_key, $new_log );
825 }
826 }
827
828 /**
829 * Remove the 'manage_charitable_settings' cap from the Campaign Manager role.
830 *
831 * @since 1.4.5
832 *
833 * @global WP_Roles
834 * @param string $action The upgrade action.
835 * @return true Will always return true.
836 */
837 public function remove_campaign_manager_cap( $action ) {
838 global $wp_roles;
839
840 if ( class_exists( 'WP_Roles' ) ) {
841 if ( ! isset( $wp_roles ) ) {
842 $wp_roles = new WP_Roles();
843 }
844 }
845
846 if ( is_object( $wp_roles ) ) {
847 $wp_roles->remove_cap( 'campaign_manager', 'manage_charitable_settings' );
848 }
849
850 return true;
851 }
852
853 /**
854 * Convert the campaign end date meta to 0 for any campaigns where it is currently blank.
855 *
856 * @since 1.4.11
857 *
858 * @global WPDB $wpdb
859 * @param string $action The upgrade action.
860 * @return boolean Whether the query was successfully executed.
861 */
862 public function fix_empty_campaign_end_date_meta( $action ) {
863 global $wpdb;
864
865 $sql = "UPDATE $wpdb->postmeta
866 INNER JOIN $wpdb->posts
867 ON $wpdb->posts.ID = $wpdb->postmeta.post_id
868 SET $wpdb->postmeta.meta_value = 0
869 WHERE $wpdb->postmeta.meta_key = '_campaign_end_date'
870 AND $wpdb->postmeta.meta_value = ''
871 AND $wpdb->posts.post_type = 'campaign';";
872
873 $ret = $wpdb->query( $sql );
874
875 return false !== $ret;
876 }
877
878 /**
879 * Clear the campaign amount donated transients.
880 *
881 * @since 1.4.18
882 *
883 * @global WPDB $wpdb
884 * @param string $action The upgrade action.
885 * @return boolean Whether the event was scheduled.
886 */
887 public function clear_campaign_amount_donated_transient( $action ) {
888 global $wpdb;
889
890 $sql = "DELETE FROM $wpdb->options
891 WHERE option_name LIKE '_transient_charitable_campaign_%_donation_amount'";
892
893 $ret = $wpdb->query( $sql );
894
895 return false !== $ret;
896 }
897
898 /**
899 * Prior to 1.4.18, when first installing Charitable all the upgrade details were
900 * stored in the upgrade log (including serialized instances of any objects used).
901 * As of 1.4.18, we only store the time, version when the upgrade took place and
902 * whether the upgrade was done at install time.
903 *
904 * @since 1.4.18
905 *
906 * @return boolean Whether the log was successfully updated.
907 */
908 public function trim_upgrade_log() {
909 $log = get_option( $this->upgrade_log_key );
910 $new_log = array();
911 $keys = array( 'time', 'version', 'install' );
912
913 foreach ( $log as $action => $details ) {
914
915 $action_log = array();
916
917 if ( array_key_exists( 'time', $details ) ) {
918 $action_log['time'] = $details['time'];
919 }
920
921 if ( array_key_exists( 'version', $details ) ) {
922 $action_log['version'] = $details['version'];
923 }
924
925 if ( array_key_exists( 'install', $details ) && $details['install'] ) {
926 $action_log['install'] = $details['install'];
927
928 /* Use the version when the plugin was installed. */
929 if ( array_key_exists( 'install', $log ) ) {
930 $action_log['version'] = $log['install']['version'];
931 }
932 }
933
934 $new_log[ $action ] = $action_log;
935
936 }
937
938 return update_option( $this->upgrade_log_key, $new_log );
939 }
940
941 /**
942 * Set a transient to display an update notice.
943 *
944 * @since 1.4.0
945 *
946 * @param array $upgrade The upgrade details.
947 * @param string $action The action key for the upgrade.
948 * @return void
949 */
950 public function set_update_notice_transient( $upgrade, $action ) {
951 set_transient( 'charitable_' . $upgrade['notice'] . '_notice', 1 );
952
953 $this->update_upgrade_log( $action );
954 }
955
956 /**
957 * Checks whether an upgrade should be completed immediately, without a prompt.
958 *
959 * @since 1.3.4
960 *
961 * @param array $upgrade The upgrade parameters.
962 * @return boolean
963 */
964 protected function do_upgrade_immediately( $upgrade ) {
965 /* If a prompt is required, return false. */
966 if ( ! isset( $upgrade['prompt'] ) || $upgrade['prompt'] ) {
967 return false;
968 }
969
970 /* If the callback is set and it's callable, return true. */
971 return isset( $upgrade['callback'] ) && is_callable( $upgrade['callback'] );
972 }
973
974 /**
975 * Checks whether an upgrade is in progress.
976 *
977 * @since 1.3.0
978 *
979 * @return false|array False if the upgrade is not in progress.
980 */
981 protected function upgrade_is_in_progress() {
982 $doing_upgrade = get_option( 'charitable_doing_upgrade', false );
983
984 if ( empty( $doing_upgrade ) ) {
985 return false;
986 }
987
988 return $doing_upgrade;
989 }
990
991 /**
992 * Finish an upgrade. This clears the charitable_doing_upgrade setting and updates the log.
993 *
994 * @since 1.3.0
995 *
996 * @param string $upgrade The upgrade action.
997 * @param string $redirect_url Optional URL to redirect to after the upgrade.
998 * @return void
999 */
1000 protected function finish_upgrade( $upgrade, $redirect_url = '' ) {
1001 delete_option( 'charitable_doing_upgrade' );
1002
1003 $this->update_upgrade_log( $upgrade );
1004
1005 if ( empty( $redirect_url ) ) {
1006 $redirect_url = admin_url( 'index.php' );
1007 }
1008
1009 wp_redirect( $redirect_url );
1010
1011 exit();
1012 }
1013
1014 /**
1015 * Add a completed upgrade to the upgrade log.
1016 *
1017 * @since 1.3.0
1018 *
1019 * @param string $upgrade The upgrade action.
1020 * @return boolean False if value was not updated and true if value was updated.
1021 */
1022 protected function update_upgrade_log( $upgrade ) {
1023 $log = get_option( $this->upgrade_log_key );
1024
1025 $log[ $upgrade ] = array(
1026 'time' => time(),
1027 'version' => charitable()->get_version(),
1028 );
1029
1030 return update_option( $this->upgrade_log_key, $log );
1031 }
1032
1033 /**
1034 * Update the campaign donations and donors tables.
1035 *
1036 * @since 1.5.4
1037 *
1038 * @return boolean Whether tables were successfully updated.
1039 */
1040 protected function update_tables() {
1041 try {
1042 charitable_get_table( 'campaign_donations' )->create_table();
1043 charitable_get_table( 'donors' )->create_table();
1044 return true;
1045 } catch ( Exception $e ) {
1046 return false;
1047 }
1048 }
1049
1050 /**
1051 * Update the donors table and add the donormeta table.
1052 *
1053 * @since 1.6.0
1054 *
1055 * @return boolean Whether tables were successfully updated.
1056 */
1057 public function upgrade_donor_tables() {
1058 try {
1059 charitable_get_table( 'donors' )->create_table();
1060 charitable_get_table( 'donormeta' )->create_table();
1061
1062 $this->finish_upgrade( 'upgrade_donor_tables' );
1063
1064 return true;
1065 } catch ( Exception $e ) {
1066 return false;
1067 }
1068 }
1069
1070 /**
1071 * Fix the donor role caps.
1072 *
1073 * @since 1.5.9
1074 *
1075 * @return true
1076 */
1077 public function fix_donor_role_caps() {
1078 remove_role( 'donor' );
1079
1080 add_role( 'donor', __( 'Donor', 'charitable' ), array( 'read' => true ) );
1081
1082 return true;
1083 }
1084
1085 /**
1086 * Update the unfiltered_html cap for Campaign Managers.
1087 *
1088 * @since 1.6.51
1089 *
1090 * @return true
1091 */
1092 public function remove_campaign_manager_unfiltered_html_cap() {
1093 global $wp_roles;
1094
1095 if ( class_exists( 'WP_Roles' ) ) {
1096 if ( ! isset( $wp_roles ) ) {
1097 $wp_roles = new WP_Roles();
1098 }
1099 }
1100
1101 if ( is_object( $wp_roles ) ) {
1102 $wp_roles->remove_cap( 'campaign_manager', 'unfiltered_html' );
1103 }
1104
1105 return true;
1106 }
1107
1108 /**
1109 * Checks whether the site has donations missing a donor id.
1110 *
1111 * @since 1.6.5
1112 *
1113 * @return boolean
1114 */
1115 protected function has_empty_donor_ids() {
1116 return 0 < $this->count_empty_donor_id_donations( get_option( 'charitable_skipped_donations_with_empty_donor_id', array() ) );
1117 }
1118
1119 /**
1120 * Count donations with empty donor ids.
1121 *
1122 * @since 1.6.6
1123 *
1124 * @param array $skipped List of skipped donations.
1125 * @return int
1126 */
1127 protected function count_empty_donor_id_donations( $skipped = array() ) {
1128 if ( empty( $skipped ) ) {
1129 return charitable()->get_db_table( 'campaign_donations' )->count_donations_by_donor( 0, true );
1130 }
1131
1132 global $wpdb;
1133
1134 $placeholders = charitable_get_query_placeholders( count( $skipped ), '%d' );
1135 $sql = "SELECT COUNT(*)
1136 FROM {$wpdb->prefix}charitable_campaign_donations
1137 WHERE donor_id = 0
1138 AND donation_id NOT IN ( $placeholders )";
1139
1140 return $wpdb->get_var( $wpdb->prepare( $sql, $skipped ) );
1141 }
1142
1143 /**
1144 * Get donations with empty donor ids.
1145 *
1146 * @since 1.6.6
1147 *
1148 * @param array $skipped List of skipped donations.
1149 * @param int $number The number of donations to retrieve.
1150 * @return int
1151 */
1152 protected function get_empty_donor_id_donations( $skipped = array(), $number = 20 ) {
1153 global $wpdb;
1154
1155 if ( empty( $skipped ) ) {
1156 return $wpdb->get_col( "SELECT DISTINCT donation_id
1157 FROM {$wpdb->prefix}charitable_campaign_donations
1158 WHERE donor_id = 0
1159 LIMIT $number;" );
1160 }
1161
1162 $placeholders = charitable_get_query_placeholders( count( $skipped ), '%d' );
1163
1164 return $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT donation_id
1165 FROM {$wpdb->prefix}charitable_campaign_donations
1166 WHERE donor_id = 0
1167 AND donation_id NOT IN ( $placeholders )
1168 LIMIT $number;", $skipped ) );
1169 }
1170
1171 /**
1172 * Fix donations with missing donor ids.
1173 *
1174 * @since 1.6.5
1175 *
1176 * @return void
1177 */
1178 public function fix_empty_donor_ids() {
1179 global $wpdb;
1180
1181 if ( ! current_user_can( 'manage_charitable_settings' ) ) {
1182 wp_die( __( 'You do not have permission to do Charitable upgrades', 'charitable' ), __( 'Error', 'charitable' ), array( 'response' => 403 ) );
1183 }
1184
1185 ignore_user_abort( true );
1186
1187 if ( ! charitable_is_func_disabled( 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) {
1188 @set_time_limit( 0 );
1189 }
1190
1191 $step = array_key_exists( 'step', $_GET ) ? absint( $_GET['step'] ) : 1;
1192 $number = 20;
1193 $skipped = get_option( 'charitable_skipped_donations_with_empty_donor_id', array() );
1194 $total = $this->count_empty_donor_id_donations( $skipped );
1195
1196 /**
1197 * If there are no donors left to remove, go ahead and wrap it up right now.
1198 */
1199 if ( ! $total ) {
1200 delete_option( 'charitable_skipped_donations_with_empty_donor_id' );
1201
1202 $this->finish_upgrade( 'fix_empty_donor_ids' );
1203 }
1204
1205 $donations_table = charitable()->get_db_table( 'campaign_donations' );
1206 $donors_table = charitable()->get_db_table( 'donors' );
1207 $donations = $this->get_empty_donor_id_donations( $skipped, $number );
1208
1209 if ( count( $donations ) ) {
1210
1211 foreach ( $donations as $donation_id ) {
1212 $data = get_post_meta( $donation_id, 'donor', true );
1213
1214 if ( ! array_key_exists( 'email', $data ) || empty( $data['email'] ) ) {
1215 $skipped[] = $donation_id;
1216 continue;
1217 }
1218
1219 $donor_id = $donors_table->get_donor_id_by_email( $data['email'] );
1220
1221 if ( ! $donor_id ) {
1222 $donor_id = $donors_table->insert( array(
1223 'email' => $data['email'],
1224 'first_name' => array_key_exists( 'first_name', $data ) ? $data['first_name'] : '',
1225 'last_name' => array_key_exists( 'last_name', $data ) ? $data['last_name'] : '',
1226 'date_joined' => get_post_field( 'post_date_gmt', $donation_id ),
1227 ) );
1228 }
1229
1230 if ( $donor_id ) {
1231 $donations_table->update( $donation_id, array(
1232 'donor_id' => $donor_id,
1233 ), 'donation_id' );
1234 } else {
1235 $skipped[] = $donation_id;
1236 }
1237 }//end foreach
1238
1239 update_option( 'charitable_skipped_donations_with_empty_donor_id', $skipped );
1240
1241 $step++;
1242
1243 $redirect = add_query_arg( array(
1244 'charitable-upgrade' => 'fix_empty_donor_ids',
1245 'page' => 'charitable-upgrades',
1246 'step' => $step,
1247 'number' => $number,
1248 'total' => $total,
1249 ), admin_url( 'index.php' ) );
1250
1251 wp_redirect( $redirect );
1252
1253 exit;
1254 }//end if
1255
1256 delete_option( 'charitable_skipped_donations_with_empty_donor_id' );
1257
1258 $this->finish_upgrade( 'fix_empty_donor_ids' );
1259 }
1260
1261 /**
1262 * HERE BE DEPRECATED FUNCTIONS...
1263 *
1264 * We're keeping these functions since Charitable add-ons extend this
1265 * class and don't know whether those have upgraded.
1266 */
1267
1268 /**
1269 * Upgrade from the current version stored in the database to the live version.
1270 *
1271 * @since 1.0.0
1272 *
1273 * @param false|string $db_version The version stored in the database.
1274 * @param string $edge_version The new version to upgrade to.
1275 * @return void
1276 */
1277 public static function upgrade_from( $db_version, $edge_version ) {
1278 if ( self::requires_upgrade( $db_version, $edge_version ) ) {
1279 new Charitable_Upgrade( $db_version, $edge_version );
1280 }
1281 }
1282
1283 /**
1284 * Perform version upgrades.
1285 *
1286 * @since 1.0.0
1287 *
1288 * @return void
1289 */
1290 protected function do_upgrades() {
1291 /**
1292 * Before Charitable 1.3, upgrades were in a simple key=>value
1293 * array format.
1294 *
1295 * $upgrade_actions = array(
1296 * '1.0.1' => 'flush_permalinks',
1297 * '1.1.0' => 'upgrade_1_1_0',
1298 * '1.1.3' => 'flush_permalinks',
1299 * '1.2.0' => 'flush_permalinks',
1300 * );
1301 */
1302
1303 if ( empty( $this->upgrade_actions ) || ! is_array( $this->upgrade_actions ) ) {
1304 return;
1305 }
1306
1307 foreach ( $this->upgrade_actions as $version => $method ) {
1308
1309 /**
1310 * The do_upgrades method was called, but the $upgrade_actions data structure has changed.
1311 *
1312 * This should never happen.
1313 */
1314 if ( is_array( $method ) ) {
1315 return;
1316 }
1317
1318 if ( self::requires_upgrade( $this->db_version, $version ) ) {
1319
1320 call_user_func( array( $this, $method ) );
1321
1322 }
1323 }
1324 }
1325
1326 /**
1327 * Saves a log of the version to version upgrades made.
1328 *
1329 * @since 1.0.0
1330 *
1331 * @return void
1332 */
1333 protected function save_upgrade_log() {
1334 $log = get_option( $this->upgrade_log_key );
1335
1336 if ( false === $log || ! is_array( $log ) ) {
1337 $log = array();
1338 }
1339
1340 $log[] = array(
1341 'timestamp' => time(),
1342 'from' => $this->db_version,
1343 'to' => $this->edge_version,
1344 );
1345
1346 update_option( $this->upgrade_log_key, $log );
1347 }
1348
1349 /**
1350 * Upgrade complete. This saves the new version to the database.
1351 *
1352 * @since 1.0.0
1353 *
1354 * @return void
1355 */
1356 protected function update_db_version() {
1357 update_option( $this->version_key, $this->edge_version );
1358 }
1359 }
1360
1361 endif;
1362