PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.2.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.2.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
suredonation / inc / admin / admin.php

admin.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.2.0, at inc/admin/admin.php

531 lines 17.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Admin Interface
4 *
5 * @package SureDonation
6 */
7
8 namespace SureDonation\Inc\Admin;
9
10 use SureDonation\Inc\Helper;
11 use SureDonation\Inc\Onboarding;
12 use SureDonation\Inc\Pdf\Pdf_Utils;
13 use SureDonation\Inc\Payments\Payment_Helper;
14 use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
15 use SureDonation\Inc\Traits\Get_Instance;
16
17 // Exit if accessed directly.
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Admin class.
24 *
25 * @since 0.0.1
26 */
27 class Admin {
28 use Get_Instance;
29
30 /**
31 * Constructor.
32 *
33 * @since 0.0.1
34 */
35 public function __construct() {
36 add_action( 'admin_menu', [ $this, 'register_menu' ] );
37 add_action( 'admin_menu', [ $this, 'hide_forms_submenu' ], 999 );
38 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
39 add_action( 'admin_init', [ $this, 'redirect_cpt_listings' ] );
40 add_action( 'admin_init', [ $this, 'register_privacy_policy_content' ] );
41 }
42
43 /**
44 * Register suggested privacy-policy text for the plugin's data transfers.
45 *
46 * Surfaces the Phone field's automatic country detection (which sends the
47 * visitor's IP to ipapi.co) in the WordPress Privacy Policy Guide
48 * (Settings → Privacy → Policy Guide) so site owners can disclose it in
49 * their own policy. This only suggests text to admins — it publishes
50 * nothing and is not shown to donors.
51 *
52 * @since 1.1.1
53 * @return void
54 */
55 public function register_privacy_policy_content() {
56 if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) {
57 return;
58 }
59
60 $content = __( 'When a Phone field with automatic country detection is used, this site sends the visitor’s IP address to a third-party service (ipapi.co) to determine their country code. The IP address is not stored for this purpose; the resulting country code is cached temporarily. Automatic detection can be disabled per Phone field, or site-wide via the “suredonation_phone_geo_enabled” filter.', 'suredonation' );
61
62 wp_add_privacy_policy_content( 'SureDonation', wp_kses_post( wpautop( $content ) ) );
63 }
64
65 /**
66 * Redirect the default CPT list screens for suredonation_form and
67 * suredonation_cmpgn to their corresponding plugin pages.
68 *
69 * For the form listing, attempt to resolve the originating campaign
70 * from the referer (the "back" arrow on the form editor sends the
71 * user here) and redirect to that campaign's single view.
72 *
73 * @return void
74 * @since 1.0.0
75 */
76 public function redirect_cpt_listings() {
77 global $pagenow;
78
79 if ( 'edit.php' !== $pagenow ) {
80 return;
81 }
82
83 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading post_type from query, no state mutation.
84 $post_type = isset( $_GET['post_type'] ) ? sanitize_key( wp_unslash( $_GET['post_type'] ) ) : '';
85
86 if ( 'suredonation_form' === $post_type ) {
87 $campaign_id = $this->resolve_campaign_id_from_referer();
88 $target = $campaign_id
89 ? admin_url( 'admin.php?page=suredonation#/campaigns/' . $campaign_id )
90 : admin_url( 'admin.php?page=suredonation#/campaigns' );
91
92 wp_safe_redirect( $target );
93 exit;
94 }
95
96 if ( 'suredonation_cmpgn' === $post_type ) {
97 wp_safe_redirect( admin_url( 'admin.php?page=suredonation#/campaigns' ) );
98 exit;
99 }
100 }
101
102 /**
103 * Resolve the campaign ID from the referer when the form list page
104 * is reached from the form editor's "back" button.
105 *
106 * @return int Campaign ID, or 0 when it cannot be determined.
107 * @since 1.0.0
108 */
109 private function resolve_campaign_id_from_referer() {
110 $referer = wp_get_referer();
111 if ( ! $referer ) {
112 return 0;
113 }
114
115 $parts = wp_parse_url( $referer );
116 if ( empty( $parts['query'] ) ) {
117 return 0;
118 }
119
120 $query = [];
121 parse_str( $parts['query'], $query );
122
123 if ( empty( $query['post'] ) || ! is_numeric( $query['post'] ) ) {
124 return 0;
125 }
126
127 $form_id = absint( $query['post'] );
128 if ( 'suredonation_form' !== get_post_type( $form_id ) ) {
129 return 0;
130 }
131
132 $campaign_id = get_post_meta( $form_id, '_suredonation_campaign_id', true );
133 if ( ! is_numeric( $campaign_id ) ) {
134 return 0;
135 }
136 return absint( $campaign_id );
137 }
138
139 /**
140 * Enqueue admin scripts and styles.
141 *
142 * @param string $hook Current admin page hook.
143 * @return void
144 * @since 0.0.1
145 */
146 public function enqueue_admin_scripts( $hook ) {
147 // Only load on our plugin pages.
148 if ( strpos( $hook, 'suredonation' ) === false ) {
149 return;
150 }
151
152 // Onboarding gets its own lean bundle — bail before enqueuing the
153 // main admin app on that page.
154 if ( false !== strpos( $hook, Onboarding::PAGE_SLUG ) ) {
155 $this->enqueue_onboarding_assets();
156 return;
157 }
158
159 $asset_file = SUREDONATION_DIR . 'assets/build/admin.asset.php';
160
161 if ( ! file_exists( $asset_file ) ) {
162 return;
163 }
164
165 $asset = include $asset_file;
166
167 // Enqueue WordPress editor for TinyMCE support.
168 wp_enqueue_editor();
169
170 // Enqueue the media library so the campaign image picker can open it.
171 wp_enqueue_media();
172
173 // Enqueue editor buttons style (needed for TinyMCE icons).
174 wp_enqueue_style( 'editor-buttons' );
175
176 // Enqueue admin app script.
177 wp_enqueue_script(
178 'suredonation-admin',
179 SUREDONATION_URL . 'assets/build/admin.js',
180 $asset['dependencies'],
181 $asset['version'],
182 true
183 );
184
185 // Enqueue admin app styles.
186 wp_enqueue_style(
187 'suredonation-admin',
188 SUREDONATION_URL . 'assets/build/admin.css',
189 [],
190 $asset['version']
191 );
192
193 // The campaign drawer sits at a very high z-index. Lift the media library
194 // above it, and keep the media modal above its OWN backdrop so the picker
195 // is not dimmed by either overlay.
196 wp_add_inline_style(
197 'suredonation-admin',
198 '.media-modal-backdrop{z-index:2147483646 !important;}.media-modal{z-index:2147483647 !important;}'
199 );
200
201 // Load JS translations for the admin app.
202 wp_set_script_translations( 'suredonation-admin', 'suredonation' );
203
204 // Localize script with plugin data.
205 wp_localize_script(
206 'suredonation-admin',
207 'suredonation_admin',
208 [
209 'version' => SUREDONATION_VER,
210 'apiUrl' => rest_url( 'suredonation/v1' ),
211 'nonce' => wp_create_nonce( 'wp_rest' ),
212 'docsURL' => 'https://suredonation.com/docs/',
213 'plugin_url' => SUREDONATION_URL,
214 'site_url' => site_url(),
215 'admin_url' => admin_url(),
216 'is_first_campaign_created' => $this->has_campaigns(),
217 'is_onboarding_completed' => Onboarding::get_instance()->is_completed(),
218 'rotating_plugin_banner' => $this->get_rotating_plugin_banner(),
219 'smart_tags' => Helper::get_smart_tags(),
220 'is_pro_active' => defined( 'SUREDONATION_PRO_VER' ),
221 'pro_plugin_version' => defined( 'SUREDONATION_PRO_VER' ) ? SUREDONATION_PRO_VER : '',
222 'pro_plugin_name' => defined( 'SUREDONATION_PRO_PRODUCT' ) ? SUREDONATION_PRO_PRODUCT : 'SureDonation Pro',
223 'is_license_active' => defined( 'SUREDONATION_PRO_VER' ) && class_exists( 'SureDonationPro\Inc\Licensing\Licensing' ) ? \SureDonationPro\Inc\Licensing\Licensing::is_license_active() : false,
224 'pdf_library_exists' => Pdf_Utils::check_if_library_exists(),
225 'php_version_compatible' => Pdf_Utils::is_php_compatible(),
226 'pdf_nonce' => wp_create_nonce( 'suredonation_pdf_nonce' ),
227 'ajax_url' => admin_url( 'admin-ajax.php' ),
228 'plugin_installer_nonce' => wp_create_nonce( 'updates' ),
229 'plugin_manager_nonce' => wp_create_nonce( 'suredonation_plugin_manager' ),
230 'has_abilities_api' => class_exists( 'WP_Ability' ),
231 'current_user_login' => wp_get_current_user()->user_login,
232 'integrations' => [
233 'sure_triggers' => Helper::get_ottokit_integration(),
234 ],
235 // Bootstrap settings data so the General currency list and the
236 // PayPal connection state render synchronously (no post-mount
237 // fetch flicker). PayPal data is the non-sensitive subset only.
238 'payments' => [
239 'currencies' => Payment_Helper::get_currencies_list(),
240 'paypal' => PayPal_Helper::get_connection_state(),
241 ],
242 ]
243 );
244 }
245
246 /**
247 * Register admin menu.
248 *
249 * @return void
250 * @since 0.0.1
251 */
252 public function register_menu() {
253 $menu_slug = 'suredonation';
254
255 // SureDonation mark for the admin menu — single path with evenodd
256 // fill so the inner heart cuts a hole that reveals the sidebar
257 // background (mirrors the pattern used by SureForms).
258 $menu_icon_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M0 3.64405C0 1.6315 1.62712 0 3.63425 0H20.2517C22.2588 0 23.886 1.6315 23.886 3.64405V20.3063C23.886 22.3188 22.2588 23.9503 20.2517 23.9503H3.63425C1.62712 23.9503 0 22.3188 0 20.3063V3.64405ZM12.6516 5.79655C14.4525 3.99075 17.3723 3.99075 19.1734 5.79655C20.9742 7.60234 20.9742 10.5301 19.1734 12.3359L17.5686 13.9451C17.2776 14.2368 16.8057 14.2368 16.5147 13.9451C16.2237 13.6533 16.2237 13.1802 16.5147 12.8884L18.1196 11.2793C19.3385 10.0571 19.3385 8.07537 18.1196 6.85312C16.9007 5.6309 14.9243 5.63092 13.7054 6.85312L11.3607 9.20415C11.0701 9.49552 11.0701 9.96791 11.3607 10.2593C11.6513 10.5506 12.1224 10.5506 12.413 10.2593L13.4659 9.20346C13.7569 8.91169 14.2288 8.91169 14.5198 9.20346C14.8107 9.49523 14.8107 9.96828 14.5198 10.26L13.4667 11.3159C12.5942 12.1909 11.1794 12.1909 10.3069 11.3159C9.43429 10.441 9.43429 9.02249 10.3069 8.14757L10.8879 7.56486L10.1781 6.85312C8.95914 5.63089 6.98277 5.63089 5.76382 6.85312C4.54486 8.07537 4.54488 10.0571 5.76382 11.2793L12.6985 18.2326C12.9895 18.5244 12.9895 18.9974 12.6985 19.2892C12.4075 19.581 11.9358 19.581 11.6448 19.2892L4.71008 12.3359C2.90915 10.5301 2.90913 7.60233 4.71008 5.79655C6.51102 3.99075 9.43091 3.99075 11.2318 5.79655L11.9417 6.50827L12.6516 5.79655ZM11.8605 14.9781C12.1515 14.6863 12.6233 14.6863 12.9143 14.9781L14.5397 16.6079C14.8307 16.8996 14.8307 17.3727 14.5397 17.6645C14.2487 17.9562 13.7769 17.9562 13.4859 17.6645L11.8605 16.0347C11.5695 15.7429 11.5695 15.2699 11.8605 14.9781ZM13.6482 13.1856C13.9392 12.8939 14.4109 12.8939 14.7019 13.1856L16.3273 14.8154C16.6183 15.1072 16.6183 15.5802 16.3273 15.872C16.0363 16.1638 15.5645 16.1638 15.2735 15.872L13.6482 14.2422C13.3572 13.9504 13.3572 13.4774 13.6482 13.1856Z" fill="#D1D5DB"/></svg>';
259
260 // Main menu page.
261 add_menu_page(
262 __( 'SureDonation', 'suredonation' ),
263 __( 'SureDonation', 'suredonation' ),
264 'manage_options',
265 $menu_slug,
266 [ $this, 'render_dashboard' ],
267 'data:image/svg+xml;base64,' . base64_encode( $menu_icon_svg ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Building data URI for menu icon.
268 25
269 );
270
271 // Register submenus.
272 $this->register_submenus( $menu_slug );
273 }
274
275 /**
276 * Render dashboard page.
277 *
278 * @return void
279 * @since 0.0.1
280 */
281 public function render_dashboard() {
282 ?>
283 <div id="suredonation-admin-root"></div>
284 <?php
285 }
286
287 /**
288 * Hide the "All Forms" submenu item.
289 *
290 * WordPress automatically adds a submenu for the suredonation_form CPT.
291 * We hide it since forms are managed through campaigns.
292 *
293 * @return void
294 * @since 0.0.1
295 */
296 public function hide_forms_submenu() {
297 remove_submenu_page( 'suredonation', 'edit.php?post_type=suredonation_form' );
298 }
299
300 /**
301 * Check if any campaigns have been created.
302 *
303 * @return bool True if at least one campaign exists.
304 * @since 0.0.1
305 */
306 private function has_campaigns() {
307 $campaigns = get_posts(
308 [
309 'post_type' => 'suredonation_cmpgn',
310 'posts_per_page' => 1,
311 'post_status' => [ 'publish', 'draft', 'private' ],
312 'fields' => 'ids',
313 ]
314 );
315
316 return ! empty( $campaigns );
317 }
318
319 /**
320 * Get rotating plugin banner data.
321 *
322 * @return array<string,mixed>|null Plugin banner data, or null when the plugin is already active.
323 * @since 0.0.1
324 */
325 private function get_rotating_plugin_banner() {
326 // Match SureForms: never nudge a plugin that is already active, so the
327 // dashboard card is hidden once SureRank is activated.
328 $status = $this->get_plugin_status( 'surerank/surerank.php' );
329 if ( 'Activated' === $status ) {
330 return null;
331 }
332
333 // Get SVG logo as base64.
334 $logo_svg = file_get_contents( SUREDONATION_DIR . 'images/surerank.svg' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file read.
335 $logo_data = $logo_svg ? 'data:image/svg+xml;base64,' . base64_encode( $logo_svg ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
336
337 // SureRank plugin data.
338 return [
339 'slug' => 'surerank',
340 'path' => 'surerank/surerank.php',
341 'title' => 'SureRank',
342 'singleLineDescription' => __( 'SEO Made Easy for Your Website', 'suredonation' ),
343 'subtitle' => __( 'Beautiful pages, persuasive content, and custom code in seconds. The possibilities are endless!', 'suredonation' ),
344 'logo' => $logo_data,
345 'status' => $status,
346 ];
347 }
348
349 /**
350 * Get plugin installation status.
351 *
352 * @param string $plugin_file Plugin file path (e.g., 'plugin-folder/plugin-file.php').
353 * @return string Plugin status: 'Activated', 'Installed', or 'Install'.
354 * @since 0.0.1
355 */
356 private function get_plugin_status( $plugin_file ) {
357 if ( ! function_exists( 'is_plugin_active' ) ) {
358 include_once ABSPATH . 'wp-admin/includes/plugin.php';
359 }
360
361 if ( is_plugin_active( $plugin_file ) ) {
362 return 'Activated';
363 }
364
365 $all_plugins = get_plugins();
366 if ( isset( $all_plugins[ $plugin_file ] ) ) {
367 return 'Installed';
368 }
369
370 return 'Install';
371 }
372
373 /**
374 * Register submenus.
375 *
376 * @param string $menu_slug Menu slug.
377 * @return void
378 * @since 0.0.1
379 */
380 private function register_submenus( $menu_slug ) {
381 $capability = 'manage_options';
382
383 $submenus = [
384 [
385 'id' => $menu_slug,
386 'page_title' => __( 'Dashboard', 'suredonation' ),
387 ],
388 [
389 'id' => 'suredonation#/campaigns',
390 'page_title' => __( 'Campaigns', 'suredonation' ),
391 ],
392 [
393 'id' => 'suredonation#/donations',
394 'page_title' => __( 'Donations', 'suredonation' ),
395 ],
396 [
397 'id' => 'suredonation#/donors',
398 'page_title' => __( 'Donors', 'suredonation' ),
399 ],
400 [
401 'id' => 'suredonation#/reports',
402 'page_title' => __( 'Reports', 'suredonation' ),
403 ],
404 [
405 'id' => 'suredonation#/settings',
406 'page_title' => __( 'Settings', 'suredonation' ),
407 ],
408 ];
409
410 // Register the submenus.
411 foreach ( $submenus as $submenu ) {
412 add_submenu_page(
413 $menu_slug,
414 $submenu['page_title'],
415 $submenu['page_title'],
416 $capability,
417 $submenu['id'],
418 [ $this, 'render_dashboard' ]
419 );
420 }
421
422 // Hidden onboarding page (no menu parent so it never shows in
423 // the sidebar but is still routable via admin.php?page=...).
424 add_submenu_page(
425 '',
426 __( 'SureDonation Setup', 'suredonation' ),
427 __( 'SureDonation Setup', 'suredonation' ),
428 $capability,
429 Onboarding::PAGE_SLUG,
430 [ $this, 'render_onboarding' ]
431 );
432 }
433
434 /**
435 * Render onboarding root container.
436 *
437 * @return void
438 * @since 1.0.0
439 */
440 public function render_onboarding() {
441 ?>
442 <div id="suredonation-onboarding-root"></div>
443 <?php
444 }
445
446 /**
447 * Enqueue the dedicated onboarding bundle.
448 *
449 * Loaded only on the onboarding admin page so the main admin app
450 * doesn't pay the cost on activation redirects.
451 *
452 * @return void
453 * @since 1.0.0
454 */
455 private function enqueue_onboarding_assets() {
456 $asset_file = SUREDONATION_DIR . 'assets/build/admin/onboarding/index.asset.php';
457
458 if ( ! file_exists( $asset_file ) ) {
459 return;
460 }
461
462 $asset = include $asset_file;
463
464 wp_enqueue_script(
465 'suredonation-onboarding',
466 SUREDONATION_URL . 'assets/build/admin/onboarding/index.js',
467 $asset['dependencies'],
468 $asset['version'],
469 true
470 );
471
472 wp_enqueue_style(
473 'suredonation-onboarding',
474 SUREDONATION_URL . 'assets/build/admin/onboarding/index.css',
475 [],
476 $asset['version']
477 );
478
479 wp_set_script_translations( 'suredonation-onboarding', 'suredonation' );
480
481 $current_user = wp_get_current_user();
482 $is_pro = defined( 'SUREDONATION_PRO_VER' );
483
484 // Detect GiveWP rows once at enqueue time so the JS doesn't have to
485 // spin a loader (and won't ever flash an error) just to decide whether
486 // to render the optional migration step.
487 $has_givewp_data = false;
488 try {
489 $has_givewp_data = \SureDonation\Inc\Import\Givewp\Source::get_instance()->has_givewp_data();
490 } catch ( \Throwable $e ) {
491 $has_givewp_data = false;
492 }
493
494 wp_localize_script(
495 'suredonation-onboarding',
496 'suredonation_onboarding',
497 [
498 'apiUrl' => rest_url( 'suredonation/v1' ),
499 'nonce' => wp_create_nonce( 'wp_rest' ),
500 'pluginUrl' => SUREDONATION_URL,
501 'adminUrl' => admin_url(),
502 'dashboardUrl' => admin_url( 'admin.php?page=suredonation' ),
503 'paymentsUrl' => admin_url( 'admin.php?page=suredonation#/settings?tab=payments' ),
504 'campaignsUrl' => admin_url( 'admin.php?page=suredonation#/campaigns' ),
505 'docsUrl' => 'https://suredonation.com/docs/',
506 'isProActive' => $is_pro,
507 'hasGiveWPData' => $has_givewp_data,
508 'currentUser' => [
509 'firstName' => $current_user ? $current_user->first_name : '',
510 'lastName' => $current_user ? $current_user->last_name : '',
511 'email' => $current_user ? $current_user->user_email : '',
512 ],
513 'privacyUrl' => 'https://suredonation.com/privacy-policy/',
514 ]
515 );
516
517 // Shim suredonation_admin on the onboarding page too — shared API
518 // helpers (e.g. importApi.js) read apiUrl + nonce from that global,
519 // so without this the migration session fails the REST cookie check.
520 wp_localize_script(
521 'suredonation-onboarding',
522 'suredonation_admin',
523 [
524 'apiUrl' => rest_url( 'suredonation/v1' ),
525 'nonce' => wp_create_nonce( 'wp_rest' ),
526 'is_pro_active' => $is_pro,
527 ]
528 );
529 }
530 }
531