PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.13.1
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.13.1
1.8.13.1 1.8.13 1.8.12.3 1.8.12.2 1.8.12.1 1.8.12 1.8.11.3 1.8.11.2 1.8.11.1 1.8.11 1.6.6 1.6.60 1.6.7 1.6.8 1.6.9 1.7.0 1.7.0.1 1.7.0.11 1.7.0.12 1.7.0.14 1.7.0.2 1.7.0.3 1.7.0.5 1.7.0.6 1.7.0.7 All 212 releases
charitable / includes / admin / tools / class-charitable-tools-misc.php

class-charitable-tools-misc.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.8.13.1, at includes/admin/tools/class-charitable-tools-misc.php

408 lines 14.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Tools Misc.
4 *
5 * @package Charitable/Classes/Charitable_Tools_Misc
6 * @author David Bisset
7 * @copyright Copyright (c) 2023, WP Charitable LLC
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.8.9
10 * @version 1.8.9
11 */
12
13 // Exit if accessed directly.
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 if ( ! class_exists( 'Charitable_Tools_Misc' ) ) :
19
20 /**
21 * Charitable_Tools_Misc
22 *
23 * @since 1.8.9
24 */
25 class Charitable_Tools_Misc {
26
27 /**
28 * The single instance of this class.
29 *
30 * @var Charitable_Tools_Misc|null
31 */
32 private static $instance = null;
33
34 /**
35 * Create object instance.
36 *
37 * @since 1.8.9
38 */
39 private function __construct() {
40 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
41 add_action( 'admin_init', array( $this, 'handle_redirect_after_save' ) );
42 }
43
44 /**
45 * Enqueue scripts for the misc tools tab.
46 *
47 * @since 1.8.9
48 *
49 * @return void
50 */
51 public function enqueue_scripts() {
52 if ( ! charitable_is_tools_view() || ! isset( $_GET['tab'] ) || 'misc' !== $_GET['tab'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
53 return;
54 }
55
56 wp_enqueue_script(
57 'charitable-tools-misc',
58 charitable()->get_path( 'assets', false ) . 'js/admin/charitable-tools-misc.js',
59 array( 'jquery' ),
60 charitable()->get_version(),
61 true
62 );
63 }
64
65 /**
66 * Returns and/or create the single instance of this class.
67 *
68 * @since 1.8.9
69 *
70 * @return Charitable_Tools_Misc
71 */
72 public static function get_instance() {
73 if ( is_null( self::$instance ) ) {
74 self::$instance = new self();
75 }
76
77 return self::$instance;
78 }
79
80 /**
81 * Add the misc tab fields.
82 *
83 * @since 1.8.9
84 *
85 * @param array $fields Existing fields.
86 * @return array
87 */
88 public function add_misc_fields( $fields = array() ) {
89 if ( ! is_array( $fields ) ) {
90 $fields = array();
91 }
92 $fields['section_tools_misc'] = array(
93 'title' => __( 'Misc', 'charitable' ),
94 'type' => 'heading',
95 'class' => 'section-heading',
96 'priority' => 5,
97 'attrs' => array(
98 'id' => 'charitable_tools_misc_section_heading',
99 ),
100 );
101 $fields['bulk_remove_donations_section'] = array(
102 'title' => __( 'Bulk Remove Donations', 'charitable' ),
103 'type' => 'heading',
104 'class' => 'section-heading',
105 'priority' => 10,
106 'attrs' => array(
107 'id' => 'charitable_tools_misc_bulk_remove_donations_section',
108 ),
109 );
110 $fields['bulk_remove_donations'] = array(
111 'label_for' => __( 'Donation Status', 'charitable' ),
112 'type' => 'select',
113 'options' => array(
114 '' => __( 'Select status...', 'charitable' ),
115 'remove_pending' => __( 'Remove all PENDING donations', 'charitable' ),
116 'remove_failed' => __( 'Remove all FAILED donations', 'charitable' ),
117 ),
118 'help' => __( 'Warning: This permanently removes all donations from all campaigns, intended for use after a spam or similar attack.', 'charitable' ),
119 'priority' => 20,
120 'attrs' => array(
121 'id' => 'charitable_tools_misc_bulk_remove_donations',
122 ),
123 );
124 $fields['bulk_remove_donations_date_from'] = array(
125 'label_for' => __( 'Date From', 'charitable' ),
126 'type' => 'date',
127 'help' => __( 'Start date for the date range. Leave empty to remove all donations of the selected status. Only used when a donation status is selected.', 'charitable' ),
128 'priority' => 30,
129 'attrs' => array(
130 'id' => 'charitable_tools_misc_bulk_remove_donations_date_from',
131 ),
132 );
133 $fields['bulk_remove_donations_date_to'] = array(
134 'label_for' => __( 'Date To', 'charitable' ),
135 'type' => 'date',
136 'help' => __( 'End date for the date range. Leave empty to remove all donations of the selected status. Only used when a donation status is selected.', 'charitable' ),
137 'priority' => 40,
138 'attrs' => array(
139 'id' => 'charitable_tools_misc_bulk_remove_donations_date_to',
140 ),
141 );
142
143 return $fields;
144 }
145
146 /**
147 * Handle option update for bulk removal.
148 *
149 * @since 1.8.9
150 *
151 * @param mixed[] $old_value The old option value.
152 * @param mixed[] $new_value The new option value.
153 * @return void
154 */
155 public function bulk_remove_donations_on_update( $old_value, $new_value ) {
156 if ( ! is_array( $new_value ) ) {
157 return;
158 }
159
160 // Check if bulk_remove_donations is set in the new values.
161 if ( ! isset( $new_value['misc'] ) || ! is_array( $new_value['misc'] ) ) {
162 return;
163 }
164
165 if ( ! isset( $new_value['misc']['bulk_remove_donations'] ) || '' === trim( $new_value['misc']['bulk_remove_donations'] ) ) {
166 return;
167 }
168
169 // Process the removal.
170 $misc_values = $new_value['misc'];
171 $result = $this->bulk_remove_donations( array(), $misc_values, $old_value );
172
173 // Update the option with the processed values to reset the fields.
174 if ( is_array( $result ) ) {
175 $new_value['misc'] = array_merge( $new_value['misc'], $result );
176 // Remove the action to prevent infinite loop.
177 remove_action( 'update_option_charitable_tools', array( $this, 'bulk_remove_donations_on_update' ), 10 );
178 update_option( 'charitable_tools', $new_value );
179 add_action( 'update_option_charitable_tools', array( $this, 'bulk_remove_donations_on_update' ), 10, 2 );
180 }
181 }
182
183 /**
184 * Removes donations based on status and optional date range.
185 *
186 * @since 1.8.9
187 *
188 * @param mixed[] $values All values, merged (when used as filter).
189 * @param mixed[] $new_values The newly submitted values.
190 * @param mixed[] $old_values The old values.
191 * @return mixed[]
192 */
193 public function bulk_remove_donations( $values, $new_values, $old_values ) {
194 if ( ! is_array( $values ) ) {
195 $values = array();
196 }
197
198 // The sanitize_settings method flattens non-dynamic groups, so misc data might be at top level.
199 // Check both structures: nested under 'misc' or flattened at top level.
200 $misc_data = null;
201 if ( isset( $new_values['misc'] ) && is_array( $new_values['misc'] ) ) {
202 // Data is nested under 'misc' (if misc is a dynamic group).
203 $misc_data = $new_values['misc'];
204 } elseif ( isset( $new_values['bulk_remove_donations'] ) ) {
205 // Data is flattened at top level (misc is not a dynamic group).
206 $misc_data = $new_values;
207 }
208
209 // If no misc data found, return unchanged.
210 if ( is_null( $misc_data ) ) {
211 return $values;
212 }
213
214 /* If this option isn't in the return values or isn't set, leave. */
215 if ( ! isset( $misc_data['bulk_remove_donations'] ) || '' === trim( $misc_data['bulk_remove_donations'] ) ) {
216 return $values;
217 }
218
219 $status = sanitize_text_field( $misc_data['bulk_remove_donations'] );
220
221 if ( ! in_array( $status, array( 'remove_pending', 'remove_failed' ), true ) ) {
222 return $values;
223 }
224
225 // Get date range if provided.
226 $date_from = isset( $misc_data['bulk_remove_donations_date_from'] ) ? sanitize_text_field( $misc_data['bulk_remove_donations_date_from'] ) : '';
227 $date_to = isset( $misc_data['bulk_remove_donations_date_to'] ) ? sanitize_text_field( $misc_data['bulk_remove_donations_date_to'] ) : '';
228
229 // Determine status value for query.
230 $donation_status = 'remove_pending' === $status ? 'charitable-pending' : 'charitable-failed';
231
232 // Build query args.
233 $query_args = array(
234 'status' => $donation_status,
235 );
236
237 // Add date range if provided.
238 if ( ! empty( $date_from ) ) {
239 // Ensure date includes time for proper comparison (start of day).
240 $date_from = gmdate( 'Y-m-d 00:00:00', strtotime( $date_from ) );
241 $query_args['start_date'] = $date_from;
242 }
243 if ( ! empty( $date_to ) ) {
244 // Ensure date includes time for proper comparison (end of day).
245 $date_to = gmdate( 'Y-m-d 23:59:59', strtotime( $date_to ) );
246 $query_args['end_date'] = $date_to;
247 }
248
249 // Get donations matching criteria.
250 $donations = charitable_get_table( 'campaign_donations' )->get_donations_report( $query_args );
251
252 if ( ! is_array( $donations ) || empty( $donations ) ) {
253 $status_label = 'remove_pending' === $status ? __( 'pending', 'charitable' ) : __( 'failed', 'charitable' );
254 charitable_get_admin_notices()->add_notice(
255 sprintf(
256 /* translators: %s: donation status */
257 __( 'No %s donations found to remove.', 'charitable' ),
258 $status_label
259 ),
260 'error',
261 false,
262 true
263 );
264 // Reset the misc fields after processing.
265 // Since misc is not a dynamic group, fields are at top level.
266 $values['bulk_remove_donations'] = false;
267 $values['bulk_remove_donations_date_from'] = false;
268 $values['bulk_remove_donations_date_to'] = false;
269 return $values;
270 }
271
272 $donations_ids = wp_list_pluck( $donations, 'donation_id' );
273 $count = count( $donations_ids );
274
275 foreach ( $donations_ids as $donation_id ) {
276
277 $remove_confirmation = wp_delete_post( $donation_id, true );
278 if ( charitable_is_debug() ) {
279 error_log( 'Bulk remove donation post: ' . $donation_id . ' - ' . print_r( $remove_confirmation, true ) ); // phpcs:ignore
280 }
281
282 $remove_confirmation = charitable_get_table( 'campaign_donations' )->delete( $donation_id );
283 if ( charitable_is_debug() ) {
284 error_log( 'Bulk remove campaign_donations: ' . $donation_id . ' - ' . $remove_confirmation ); // phpcs:ignore
285 }
286
287 $remove_confirmation = charitable_get_table( 'campaign_donations' )->delete_donation_records( $donation_id );
288 if ( charitable_is_debug() ) {
289 error_log( 'Bulk remove donation records: ' . $donation_id . ' - ' . $remove_confirmation ); // phpcs:ignore
290 }
291
292 $remove_confirmation = charitable_get_table( 'donation_activities' )->delete_by( 'donation_id', $donation_id );
293 if ( charitable_is_debug() ) {
294 error_log( 'Bulk remove activity records: ' . $donation_id . ' - ' . $remove_confirmation ); // phpcs:ignore
295 }
296 }
297
298 // Allow an addon to hook into this.
299 do_action( 'charitable_after_bulk_remove_donations', $status, $count, $date_from, $date_to );
300
301 $status_label = 'remove_pending' === $status ? __( 'pending', 'charitable' ) : __( 'failed', 'charitable' );
302 charitable_get_admin_notices()->add_notice(
303 sprintf(
304 /* translators: %1$d: count, %2$s: status */
305 _n(
306 '%1$d %2$s donation has been deleted.',
307 '%1$d %2$s donations have been deleted.',
308 $count,
309 'charitable'
310 ),
311 $count,
312 $status_label
313 ),
314 'success',
315 false,
316 true
317 );
318
319 // Reset the misc fields after processing.
320 // Since misc is not a dynamic group, fields are at top level.
321 $values['bulk_remove_donations'] = false;
322 $values['bulk_remove_donations_date_from'] = false;
323 $values['bulk_remove_donations_date_to'] = false;
324
325 // Allow an addon to hook into this.
326 do_action( 'charitable_end_bulk_remove_donations', $values, $new_values );
327
328 return $values;
329 }
330
331 /**
332 * Handle redirect after saving tools to ensure we stay on the misc tab.
333 *
334 * @since 1.8.9
335 *
336 * @return void
337 */
338 public function handle_redirect_after_save() {
339 // Only process on admin_init when we're on options.php or after redirect.
340 if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
341 return;
342 }
343
344 $is_options_page = false !== strpos( $_SERVER['REQUEST_URI'], 'options.php' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
345 $is_settings_updated = isset( $_GET['settings-updated'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
346
347 if ( ! $is_options_page && ! $is_settings_updated ) {
348 return;
349 }
350
351 // Check if we have misc fields in the POST data (for options.php) or if we just saved.
352 if ( $is_options_page ) {
353 if ( ! isset( $_POST['charitable_tools'] ) || ! isset( $_POST['charitable_tools']['misc'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
354 return;
355 }
356
357 // Check if bulk_remove_donations was submitted.
358 $bulk_remove_donations = isset( $_POST['charitable_tools']['misc']['bulk_remove_donations'] ) ? sanitize_text_field( wp_unslash( $_POST['charitable_tools']['misc']['bulk_remove_donations'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
359 if ( '' === trim( $bulk_remove_donations ) ) {
360 return;
361 }
362
363 // Add filter to modify redirect URL after options are saved.
364 add_filter( 'wp_redirect', array( $this, 'filter_redirect_url' ), 10, 2 );
365 } elseif ( $is_settings_updated ) {
366 // If we're on a settings-updated page but not on misc tab, redirect to misc.
367 $current_tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
368 if ( 'misc' !== $current_tab && isset( $_GET['page'] ) && 'charitable-tools' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
369 // Check if we have a transient indicating we just processed misc.
370 $misc_processed = get_transient( 'charitable_tools_misc_processed' );
371 if ( $misc_processed ) {
372 delete_transient( 'charitable_tools_misc_processed' );
373 wp_safe_redirect( admin_url( 'admin.php?page=charitable-tools&tab=misc&settings-updated=true' ) );
374 exit;
375 }
376 }
377 }
378 }
379
380 /**
381 * Filter the redirect URL to ensure we go to the misc tab.
382 *
383 * @since 1.8.9
384 *
385 * @param string $location The redirect location.
386 * @param int $status The redirect status code.
387 * @return string
388 */
389 public function filter_redirect_url( $location, $status ) {
390 // Only filter if we have misc bulk removal data in POST.
391 if ( ! isset( $_POST['charitable_tools']['misc']['bulk_remove_donations'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
392 return $location;
393 }
394
395 // Set transient to indicate misc was processed.
396 set_transient( 'charitable_tools_misc_processed', true, 30 );
397
398 // Replace the location with the misc tab URL.
399 $location = admin_url( 'admin.php?page=charitable-tools&tab=misc&settings-updated=true' );
400 remove_filter( 'wp_redirect', array( $this, 'filter_redirect_url' ), 10 );
401
402 return $location;
403 }
404 }
405
406 endif;
407
408