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 / admin / tools / class-charitable-tools.php

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

622 lines 17.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Tools UI.
4 *
5 * @package Charitable/Classes/Charitable_Tools
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.1.6
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' ) ) :
19
20 /**
21 * Charitable_Tools
22 *
23 * @final
24 * @since 1.8.1.6
25 */
26 class Charitable_Tools {
27
28 /**
29 * The single instance of this class.
30 *
31 * @var Charitable_Tools|null
32 */
33 private static $instance = null;
34
35
36 /**
37 * The dynamic groups.
38 *
39 * @var array
40 */
41 public $dynamic_groups;
42
43 /**
44 * Create object instance.
45 *
46 * @since 1.8.1.6
47 * @version 1.8.9
48 */
49 public function __construct() {
50
51 // Set the default tab.
52 // Only redirect if we're on the tools page (not options.php) and no tab is set.
53 // Don't redirect when processing form submissions (options.php).
54 $is_options_page = isset( $_SERVER['REQUEST_URI'] ) && false !== strpos( $_SERVER['REQUEST_URI'], 'options.php' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
55 if ( charitable_is_tools_view() && ! isset( $_GET['tab'] ) && ! $is_options_page ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
56 $link = admin_url( 'edit-tags.php?taxonomy=campaign_category&post_type=campaign' );
57 wp_safe_redirect( $link );
58 exit();
59 }
60
61 add_action( 'campaign_category_pre_add_form', array( $this, 'add_tools_nav_to_taxononmy_pages' ), 10, 1 );
62 add_action( 'campaign_tag_pre_add_form', array( $this, 'add_tools_nav_to_taxononmy_pages' ), 10, 1 );
63 add_action( 'campaign_category_pre_edit_form', array( $this, 'add_tools_nav_to_taxononmy_pages' ), 10, 1 );
64 add_action( 'campaign_tag_pre_edit_form', array( $this, 'add_tools_nav_to_taxononmy_pages' ), 10, 1 );
65 add_action( 'admin_init', array( $this, 'taxonomy_redirects' ), 10, 1 );
66 }
67
68 /**
69 * Redirects to the correct taxonomy page.
70 *
71 * @since 1.8.2
72 * @version 1.8.9
73 *
74 * @return void
75 */
76 public function taxonomy_redirects() {
77 if ( isset( $_GET['page'] ) && 'charitable-tools' == $_GET['page'] && isset( $_GET['tab'] ) && 'categories' == $_GET['tab'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
78 $link = admin_url( 'edit-tags.php?taxonomy=campaign_category&post_type=campaign' );
79 wp_safe_redirect( $link );
80 exit();
81 }
82
83 if ( isset( $_GET['page'] ) && 'charitable-tools' == $_GET['page'] && isset( $_GET['tab'] ) && 'tags' == $_GET['tab'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
84 $link = admin_url( 'edit-tags.php?taxonomy=campaign_tag&post_type=campaign' );
85 wp_safe_redirect( $link );
86 exit();
87 }
88 }
89
90 /**
91 * Returns and/or create the single instance of this class.
92 *
93 * @since 1.8.1.6
94 *
95 * @return Charitable_Tools
96 */
97 public static function get_instance() {
98 if ( is_null( self::$instance ) ) {
99 self::$instance = new self();
100 }
101
102 return self::$instance;
103 }
104
105 /**
106 * Run things upon init.
107 *
108 * @since 1.8.1.6
109 *
110 * @return void
111 */
112 public function init() {
113 }
114
115 /**
116 * Return the sub-sections for the Import tab.
117 *
118 * @since 1.8.10
119 *
120 * @return array
121 */
122 public function get_sub_sections_import() {
123 /**
124 * Filter the import sub-tabs.
125 *
126 * @since 1.8.10
127 *
128 * @param array $sub_tabs List of sub-tabs in key=>label format.
129 */
130 return apply_filters(
131 'charitable_tools_import_sub_tabs',
132 array(
133 'import__charitable' => __( 'Charitable', 'charitable' ),
134 'import__givewp' => __( 'GiveWP', 'charitable' ),
135 'import__givebutter' => __( 'GiveButter', 'charitable' ),
136 )
137 );
138 }
139
140 /**
141 * Redirect to default sub-tab when on the import tab without a sub_tab parameter.
142 *
143 * @since 1.8.10
144 *
145 * @return void
146 */
147 public function tools_sub_tab_default() {
148 // Don't redirect during form submissions.
149 if ( ! empty( $_POST ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
150 return;
151 }
152
153 if ( ! charitable_is_tools_view( 'import' ) ) {
154 return;
155 }
156
157 if ( isset( $_GET['sub_tab'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
158 return;
159 }
160
161 $link = admin_url( 'admin.php?page=charitable-tools&tab=import&sub_tab=charitable' );
162 wp_safe_redirect( $link );
163 exit();
164 }
165
166 /**
167 * Return the array of tabs used on the settings page.
168 *
169 * @since 1.8.1.6
170 *
171 * @return string[]
172 */
173 public function get_sections() {
174 /**
175 * Filter the settings tabs.
176 *
177 * @since 1.8.1.6
178 *
179 * @param string[] $tabs List of tabs in key=>label format.
180 */
181 return apply_filters(
182 'charitable_tools_tabs',
183 array(
184 'categories' => __( 'Categories', 'charitable' ),
185 'tags' => __( 'Tags', 'charitable' ),
186 'customize' => __( 'Customize', 'charitable' ),
187 'export' => __( 'Export', 'charitable' ),
188 'import' => __( 'Import', 'charitable' ),
189 'ai-mcp' => __( 'AI MCP', 'charitable' ),
190 'system-info' => __( 'System Info', 'charitable' ),
191 'snippets' => __( 'Code Snippets', 'charitable' ),
192 'logs' => __( 'Logs', 'charitable' ),
193 'misc' => __( 'Misc', 'charitable' ),
194 )
195 );
196 }
197
198 /**
199 * The "New" badge markup for a Tools tab, or '' when it has none.
200 *
201 * A method rather than HTML inside the get_sections() label, because the
202 * two navs that render those labels escape them differently -
203 * views/tools/tools.php echoes the label raw, the taxonomy-page nav in
204 * this class runs it through esc_html() - so markup smuggled into the
205 * label would render in one place and print as literal tags in the
206 * other. Keeping labels plain text and the badge separate means both
207 * navs can escape correctly.
208 *
209 * It also means ONE place decides which tab is new. Both navs call this,
210 * the same way both now loop get_sections(); a second hand-written copy
211 * is exactly how the AI MCP tab went missing from the taxonomy screens
212 * in the first place.
213 *
214 * Removing the badge is a manual edit next cycle, matching how
215 * `.charitable-menu-new-indicator` is used on the Donors menu item -
216 * there is no dismissal state and it is not tied to a version check, so
217 * it stays until somebody takes it out.
218 *
219 * @since 1.8.13
220 *
221 * @param string $tab_key The tab key from get_sections().
222 * @return string Badge HTML, already escaped, or '' for every other tab.
223 */
224 public function get_section_badge( $tab_key ) {
225
226 if ( 'ai-mcp' !== $tab_key ) {
227 return '';
228 }
229
230 return ' <span class="charitable-tab-new-badge">' . esc_html__( 'New', 'charitable' ) . '</span>';
231 }
232
233 /**
234 * Register setting.
235 *
236 * @since 1.0.0
237 *
238 * @return void
239 */
240 public function register_settings() {
241
242 if ( ! charitable_is_tools_view() ) {
243 return;
244 }
245
246 register_setting( 'charitable_tools', 'charitable_tools', array( $this, 'sanitize_settings' ) );
247
248 $fields = $this->get_fields();
249
250 if ( empty( $fields ) ) {
251 return;
252 }
253
254 $sections = array_merge( $this->get_sections(), $this->get_sub_sections_import(), $this->get_dynamic_groups() );
255
256 /* Register each section */
257 foreach ( $sections as $section_key => $section ) {
258 $section_id = 'charitable_tools_' . $section_key;
259
260 add_settings_section(
261 $section_id,
262 __return_null(),
263 '__return_false',
264 $section_id
265 );
266
267 if ( ! isset( $fields[ $section_key ] ) || empty( $fields[ $section_key ] ) ) {
268 continue;
269 }
270
271 /* Sort by priority */
272 $section_fields = $fields[ $section_key ];
273 uasort( $section_fields, 'charitable_priority_sort' );
274
275 /* Add the individual fields within the section */
276 foreach ( $section_fields as $key => $field ) {
277 $this->register_field( $field, array( $section_key, $key ) );
278 }
279 }
280 }
281
282 /**
283 * Return list of dynamic groups.
284 *
285 * @since 1.8.1.6
286 *
287 * @return string[]
288 */
289 private function get_dynamic_groups() {
290 if ( ! isset( $this->dynamic_groups ) ) {
291 /**
292 * Filter the list of dynamic groups.
293 *
294 * @since 1.8.1.6
295 *
296 * @param array $groups The dynamic groups.
297 */
298 $this->dynamic_groups = apply_filters( 'charitable_dynamic_tools_groups', array() );
299 }
300
301 return $this->dynamic_groups;
302 }
303
304 /**
305 * Returns whether the given key indicates the start of a new section of the settings.
306 *
307 * @since 1.0.0
308 *
309 * @param string $composite_key The unique key for this group.
310 * @return boolean
311 */
312 private function is_dynamic_group( $composite_key ) {
313 return array_key_exists( $composite_key, $this->get_dynamic_groups() );
314 }
315
316 /**
317 * Return the label for the given field.
318 *
319 * @since 1.0.0
320 *
321 * @param array $field The field definition.
322 * @param string $key The field key.
323 * @return string
324 */
325 private function get_field_label( $field, $key ) { // phpcs:ignore
326 $label = '';
327
328 if ( isset( $field['label_for'] ) ) {
329 $label = $field['label_for'];
330 }
331
332 if ( isset( $field['title'] ) ) {
333 $label = $field['title'];
334 }
335
336 return $label;
337 }
338
339 /**
340 * Return a space separated string of classes for the given field.
341 *
342 * @since 1.0.0
343 *
344 * @param array $field Field definition.
345 * @return string
346 */
347 private function get_field_classes( $field ) {
348 $classes = array( 'charitable-settings-field' );
349
350 if ( isset( $field['class'] ) ) {
351 $classes[] = $field['class'];
352 }
353
354 /**
355 * Filter the list of classes to apply to settings fields.
356 *
357 * @since 1.0.0
358 *
359 * @param array $classes The list of classes.
360 * @param array $field The field definition.
361 */
362 $classes = apply_filters( 'charitable_settings_field_classes', $classes, $field );
363
364 return implode( ' ', $classes );
365 }
366
367 /**
368 * Return an array with all the fields & sections to be displayed.
369 *
370 * @uses charitable_settings_fields
371 * @see Charitable_Settings::register_setting()
372 * @since 1.0.0
373 *
374 * @return array
375 */
376 private function get_fields() {
377 /**
378 * Use the charitable_settings_tab_fields to include the fields for new tabs.
379 * DO NOT use it to add individual fields. That should be done with the
380 * filters within each of the methods.
381 */
382 $fields = array();
383
384 $all_sections = array_merge( $this->get_sections(), $this->get_sub_sections_import() );
385
386 foreach ( $all_sections as $section_key => $section ) {
387 /**
388 * Filter the array of fields to display in a particular tab.
389 *
390 * For sub-tabs, uses the filter name charitable_tools_tab_fields_sub_{key}.
391 *
392 * @since 1.0.0
393 *
394 * @param array $fields Array of fields.
395 */
396 $filter_name = ( false !== strpos( $section_key, '__' ) )
397 ? 'charitable_tools_tab_fields_sub_' . $section_key
398 : 'charitable_tools_tab_fields_' . $section_key;
399
400 $fields[ $section_key ] = apply_filters( $filter_name, array() );
401 }
402
403 /**
404 * Filter the array of settings fields.
405 *
406 * @since 1.0.0
407 *
408 * @param array $fields Array of fields.
409 */
410 return apply_filters( 'charitable_tools_tab_fields', $fields );
411 }
412
413 /**
414 * Recursively add settings fields, given an array.
415 *
416 * @since 1.0.0
417 *
418 * @param array $field The setting field.
419 * @param array $keys Array containing the section key and field key.
420 * @return void
421 */
422 private function register_field( $field, $keys ) {
423 $section_id = 'charitable_tools_' . $keys[0];
424
425 if ( isset( $field['render'] ) && ! $field['render'] ) {
426 return;
427 }
428
429 /* Drop the first key, which is the section identifier */
430 $field['name'] = implode( '][', $keys );
431
432 if ( ! $this->is_dynamic_group( $keys[0] ) ) {
433 array_shift( $keys );
434 }
435
436 $field['key'] = $keys;
437 $field['classes'] = $this->get_field_classes( $field );
438 $callback = isset( $field['callback'] ) ? $field['callback'] : array( $this, 'render_field' );
439 $label = $this->get_field_label( $field, end( $keys ) );
440
441 add_settings_field(
442 sprintf( 'charitable_tools_%s', implode( '_', $keys ) ),
443 $label,
444 $callback,
445 $section_id,
446 $section_id,
447 $field
448 );
449 }
450
451 /**
452 * Sanitize settings before saving.
453 *
454 * @since 1.8.9
455 *
456 * @param array $values Submitted values.
457 * @return array
458 */
459 public function sanitize_settings( $values ) {
460 $old_values = get_option( 'charitable_tools', array() );
461 $new_values = array();
462
463 if ( ! is_array( $old_values ) ) {
464 $old_values = array();
465 }
466
467 if ( ! is_array( $values ) ) {
468 $values = array();
469 }
470
471 // Merge submitted values into the master array.
472 foreach ( $values as $section => $submitted ) {
473 if ( ! is_array( $submitted ) ) {
474 continue;
475 }
476 foreach ( $submitted as $key => $value ) {
477 if ( $this->is_dynamic_group( $section ) ) {
478 $new_values[ $section ][ $key ] = $value;
479 } else {
480 $new_values[ $key ] = $value;
481 }
482 }
483 }
484
485 $values = wp_parse_args( $new_values, $old_values );
486
487 if ( charitable_is_debug() ) {
488 error_log( 'sanitize_settings tools' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
489 error_log( 'values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
490 error_log( print_r( $values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
491 error_log( 'old_values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
492 error_log( print_r( $old_values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
493 error_log( 'new_values:' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
494 error_log( print_r( $new_values, true ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log,WordPress.PHP.DevelopmentFunctions.error_log_print_r
495 }
496
497 /**
498 * Filter sanitized tools settings.
499 *
500 * @since 1.8.9
501 *
502 * @param array $values All values, merged.
503 * @param array $new_values Newly submitted values.
504 * @param array $old_values Old settings.
505 */
506 $values = apply_filters( 'charitable_save_tools', $values, $new_values, $old_values );
507
508 return $values;
509 }
510
511 /**
512 * Checkbox settings should always be either 1 or 0.
513 *
514 * @since 1.0.0
515 *
516 * @param mixed $value Submitted value for field.
517 * @param array $field Field definition.
518 * @return int
519 */
520 public function sanitize_checkbox_value( $value, $field ) {
521 if ( isset( $field['type'] ) && 'checkbox' == $field['type'] ) {
522 $value = intval( $value && 'on' == $value );
523 }
524
525 return $value;
526 }
527
528 /**
529 * Render field. This is the default callback used for all fields, unless an alternative callback has been specified.
530 *
531 * @since 1.0.0
532 *
533 * @param array $args Field definition.
534 * @return void
535 */
536 public function render_field( $args ) {
537 $field_type = isset( $args['type'] ) ? $args['type'] : 'text';
538
539 charitable_admin_view( 'settings/' . $field_type, $args );
540 }
541
542 /**
543 * Returns an array of all pages in the id=>title format.
544 *
545 * @since 1.0.0
546 *
547 * @return string[]
548 */
549 public function get_pages() {
550 if ( ! isset( $this->pages ) ) {
551 $this->pages = charitable_get_pages_options();
552 }
553
554 return $this->pages;
555 }
556
557 /**
558 * Add tools nav to taxonomy pages.
559 *
560 * @since 1.8.2
561 *
562 * @param string $taxonomy The taxonomy.
563 *
564 * @return void
565 */
566 public function add_tools_nav_to_taxononmy_pages( $taxonomy = false ) { // phpcs:ignore
567
568 ob_start();
569
570 $taxonomy_now = isset( $_GET['taxonomy'] ) ? $_GET['taxonomy'] : ''; // phpcs:ignore
571
572 /*
573 * Driven from get_sections() rather than a hand-written list.
574 *
575 * This nav is a SECOND copy of the one in views/tools/tools.php,
576 * needed because the Categories and Tags tabs redirect out to
577 * edit-tags.php (see taxonomy_redirects()), where that view never
578 * runs. It used to hardcode nine anchors, and so it silently went
579 * stale every time a tab was added: `ai-mcp` landed in
580 * get_sections() in 1.8.13 and never appeared here, which is
581 * exactly how the AI MCP tab went missing on the two taxonomy
582 * screens while showing correctly everywhere else. Looping the same
583 * source both navs share is what stops the next tab drifting too.
584 *
585 * Only the two taxonomy tabs need a non-standard href, since they
586 * are the ones that live outside page=charitable-tools.
587 */
588 $taxonomy_tabs = array(
589 'categories' => 'campaign_category',
590 'tags' => 'campaign_tag',
591 );
592
593 ?>
594
595 <div id="charitable-tools-nav">
596 <h1><?php echo esc_html__( 'Charitable Tools', 'charitable' ); ?></h1>
597 <h2 class="nav-tab-wrapper">
598 <?php foreach ( charitable_get_admin_tools()->get_sections() as $tab_key => $tab_label ) : ?>
599 <?php
600 if ( isset( $taxonomy_tabs[ $tab_key ] ) ) {
601 $tab_url = admin_url( 'edit-tags.php?taxonomy=' . $taxonomy_tabs[ $tab_key ] . '&post_type=campaign' );
602 $tab_active = $taxonomy_tabs[ $tab_key ] === $taxonomy_now ? 'nav-tab-active' : '';
603 } else {
604 $tab_url = admin_url( 'admin.php?page=charitable-tools&tab=' . $tab_key );
605 $tab_active = '';
606 }
607 ?>
608 <a href="<?php echo esc_url( $tab_url ); ?>" class="nav-tab <?php echo esc_attr( $tab_active ); ?>"><?php echo esc_html( $tab_label ); ?><?php echo $this->get_section_badge( $tab_key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in get_section_badge(). ?></a>
609 <?php endforeach; ?>
610 </h2>
611 </div>
612
613 <?php
614
615 $content = ob_get_clean();
616
617 echo $content; // phpcs:ignore
618 }
619 }
620
621 endif;
622