PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.8.12.2
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.8.12.2
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 / class-charitable-admin-connect.php

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

841 lines 31.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Charitable Admin Connect.
4 *
5 * @package Charitable/Classes/Charitable_Admin_Connect
6 * @since 1.8.5
7 * @version 1.8.9.6
8 * @category Class
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 /**
16 * Charitable_Admin_Getting_Started page class.
17 *
18 * This page is shown when the plugin is first activated.
19 *
20 * @since 1.8.5
21 * @version 1.8.9.6
22 */
23 class Charitable_Admin_Connect {
24
25 /**
26 * Charitable Pro plugin basename.
27 *
28 * @since 1.8.5
29 *
30 * @var string
31 */
32 const PRO_PLUGIN = 'charitable-pro/charitable.php';
33
34 /**
35 * The single instance of this class.
36 *
37 * @var Charitable_Admin_Connect|null
38 */
39 private static $instance = null;
40
41 /**
42 * Primary class constructor.
43 *
44 * @since 1.8.5
45 */
46 public function __construct() {
47 }
48
49 /**
50 * Hooks.
51 *
52 * @since 1.8.5
53 */
54 public function hooks() {
55 }
56
57 /**
58 * Settings page enqueues.
59 *
60 * @since 1.8.5
61 * @version 1.8.9.6
62 *
63 * @param string $min Minified suffix.
64 * @param string $version Charitable version.
65 * @param string $assets_dir Assets directory.
66 */
67 public function settings_enqueues( $min, $version, $assets_dir ) { // phpcs:ignore
68
69 // Ensure jquery-confirm is available so $.alert() modals (Pro install, activate, etc.) can show.
70 if ( ! wp_script_is( 'jquery-confirm', 'registered' ) ) {
71 wp_register_script(
72 'jquery-confirm',
73 charitable()->get_path( 'directory', false ) . 'assets/lib/jquery.confirm/jquery-confirm.min.js',
74 array( 'jquery' ),
75 '3.3.4',
76 false
77 );
78 wp_register_style(
79 'jquery-confirm',
80 charitable()->get_path( 'directory', false ) . 'assets/lib/jquery.confirm/jquery-confirm.min.css',
81 null,
82 '3.3.4'
83 );
84 }
85 wp_enqueue_script( 'jquery-confirm' );
86 wp_enqueue_style( 'jquery-confirm' );
87
88 wp_enqueue_script(
89 'charitable-connect',
90 charitable()->get_path( 'assets', false ) . "js/admin/charitable-admin-connect{$min}.js",
91 array( 'jquery', 'jquery-confirm' ),
92 $version,
93 true
94 );
95 }
96
97 /**
98 * Generate and return Charitable Connect URL.
99 *
100 * @since 1.8.5
101 * @version 1.8.9.6
102 */
103 public function generate_url() {
104
105 // Run a security check.
106 check_ajax_referer( 'charitable-admin', 'nonce' );
107
108 // Check for permissions.
109 if ( ! current_user_can( 'install_plugins' ) ) {
110 wp_send_json_error( array( 'message' => esc_html__( 'You are not allowed to install plugins.', 'charitable' ) ) );
111 }
112
113 $current_plugin = plugin_basename( charitable()->get_path() . 'charitable.php' );
114 $is_pro = charitable_is_pro();
115
116 $key = ! empty( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
117
118 // Empty license key.
119 if ( empty( $key ) ) {
120 wp_send_json_error(
121 array(
122 'show_manual_upgrade' => true,
123 'url' => 'https://www.wpcharitable.com/documentation/installing-extensions/',
124 'message' => esc_html__( 'Please enter your license key to connect.', 'charitable' ),
125 )
126 );
127 }
128
129 // Whether it is the pro version.
130 if ( $is_pro ) {
131 wp_send_json_error( array( 'message' => esc_html__( 'Only the Lite version of Charitable can be upgraded.', 'charitable' ) ) );
132 }
133
134 // STEP 1: VALIDATE LICENSE FIRST (regardless of Pro plugin existence)
135 $license_data = charitable_get_helper( 'licenses' )->verify_license( 'charitable', $key, true, false );
136
137 if ( charitable_is_debug() ) {
138 error_log( 'CHARITABLE DEBUG: verify_license returned: ' . print_r( $license_data, true ) ); // phpcs:ignore
139 }
140
141 // API returns: $license_data['license'] = the license key string
142 // $license_data['valid'] = 1 for valid, 0 for invalid
143 if ( empty( $license_data ) || ! $license_data['valid'] ) {
144 if ( charitable_is_debug() ) {
145 error_log( 'CHARITABLE DEBUG: License validation FAILED' ); // phpcs:ignore
146 }
147 wp_send_json_error( array(
148 'message' => esc_html__( 'Invalid license key. Please check your license key and try again.', 'charitable' )
149 ) );
150 }
151
152 if ( charitable_is_debug() ) {
153 error_log( 'CHARITABLE DEBUG: License validation PASSED' ); // phpcs:ignore
154 }
155
156 // STEP 2: STORE VALID LICENSE IMMEDIATELY
157 $settings = get_option( 'charitable_settings', array() );
158 if ( ! isset( $settings['licenses'] ) ) {
159 $settings['licenses'] = array();
160 }
161
162 // Use keys returned by verify_license(): expiration_date, plan_id (not expires/price_id).
163 $settings['licenses']['charitable-v2'] = array(
164 'license' => $key,
165 'expiration_date' => isset( $license_data['expiration_date'] ) ? $license_data['expiration_date'] : false,
166 'plan_id' => isset( $license_data['plan_id'] ) ? $license_data['plan_id'] : false,
167 'valid' => true,
168 );
169
170 update_option( 'charitable_settings', $settings );
171
172 // STEP 2.5: CHECK LOCALHOST AFTER LICENSE VALIDATION
173 // Now that we have a valid license, handle localhost differently (unless testing: ?charitable_connect_test=1).
174 $connect_test = ! empty( $_POST['charitable_connect_test'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
175 if ( charitable_is_localhost() && ! $connect_test ) {
176 wp_send_json_success(
177 array(
178 'show_manual_upgrade' => true,
179 'url' => 'https://www.wpcharitable.com/documentation/installing-extensions/',
180 'message' => esc_html__( 'License validated successfully! Since you\'re on localhost, please install Charitable Pro manually using the link below.', 'charitable' ),
181 'license_validated' => true,
182 )
183 );
184 }
185
186 // STEP 2.6: PRO INSTALLED BUT INACTIVE — show popup to activate; do not go to upgrade.wpcharitable.com.
187 $pro_plugin_path = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN;
188 $pro_is_active = file_exists( $pro_plugin_path ) && in_array( self::PRO_PLUGIN, (array) get_option( 'active_plugins', array() ), true );
189 if ( file_exists( $pro_plugin_path ) && ! $pro_is_active ) {
190 $redirect_url = admin_url( 'admin.php?page=charitable-settings&tab=general' );
191 wp_send_json_success(
192 array(
193 'show_activate_pro_popup' => true,
194 'message' => esc_html__( 'Your license is valid. Pro is installed but inactive. Activate now?', 'charitable' ),
195 'redirect_url' => $redirect_url,
196 'plugin_basename' => self::PRO_PLUGIN,
197 )
198 );
199 }
200
201 // STEP 3: REDIRECT TO UPGRADE.WPCHARITABLE.COM (Pro not installed — need to download)
202 // Build signed URL; upgrade site will validate and call back to process() with the Pro download URL.
203 $oth_raw = wp_generate_password( 64, true, true );
204 $oth_signed = hash_hmac( 'sha512', $oth_raw, wp_salt() );
205 update_option( 'charitable_connect_token', $oth_raw );
206 update_option( 'charitable_connect', $key );
207
208 $redirect_back = admin_url( 'admin.php?page=charitable-settings&tab=general' );
209 $endpoint = $this->get_connect_callback_endpoint();
210
211 // Upgrade site expects redirect to be base64-encoded (see upgrade.wpcharitable.com index.php).
212 $upgrade_url = add_query_arg(
213 array(
214 'key' => $key,
215 'oth' => $oth_signed,
216 'endpoint' => $endpoint,
217 'version' => charitable()->get_version(),
218 'siteurl' => site_url(),
219 'homeurl' => home_url(),
220 'redirect' => base64_encode( $redirect_back ),
221 ),
222 'https://upgrade.wpcharitable.com/'
223 );
224
225 wp_send_json_success( array( 'upgrade_url' => $upgrade_url ) );
226 }
227
228 /**
229 * Process Charitable Connect.
230 *
231 * @since 1.8.5
232 * @version 1.8.9.6
233 */
234 public function process() {
235
236 $error = esc_html__( 'There was an error while installing an upgrade. Please download the plugin from charitable.com and install it manually.', 'charitable' );
237
238 if ( charitable_is_debug() ) {
239 error_log( 'Charitable Admin Connect process' ); // phpcs:ignore
240 error_log( print_r( $_REQUEST, true ) ); // phpcs:ignore
241 }
242
243 // Verify params present (oth & download link).
244 $post_oth = ! empty( $_REQUEST['oth'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['oth'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
245 $post_url = ! empty( $_REQUEST['file'] ) ? esc_url_raw( wp_unslash( $_REQUEST['file'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
246 $post_key = ! empty( $_REQUEST['key'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['key'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
247 $post_item_id = ! empty( $_REQUEST['item_id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['item_id'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
248
249 if ( empty( $post_oth ) || empty( $post_url ) ) {
250 if ( charitable_is_debug() ) {
251 error_log( 'Charitable Connect process: FAIL Missing oth or file (callback must be called by upgrade server with POST oth + file).' ); // phpcs:ignore
252 }
253 wp_send_json_error( esc_html__( 'Missing verification token or download URL. This endpoint must be called by the upgrade server.', 'charitable' ) );
254 }
255
256 // Verify oth.
257 $oth = get_option( 'charitable_connect_token' );
258
259 if ( empty( $oth ) ) {
260 if ( charitable_is_debug() ) {
261 error_log( 'Charitable Connect process: FAIL No oth found (charitable_connect_token empty in DB).' ); // phpcs:ignore
262 }
263 wp_send_json_error( esc_html__( 'No oth found.', 'charitable' ) );
264 }
265
266 if ( hash_hmac( 'sha512', $oth, wp_salt() ) !== $post_oth ) {
267 if ( charitable_is_debug() ) {
268 error_log( 'Charitable Connect process: FAIL Invalid oth (HMAC mismatch).' ); // phpcs:ignore
269 }
270 wp_send_json_error( esc_html__( 'Invalid oth.', 'charitable' ) );
271 }
272
273 // Delete so cannot replay.
274 delete_option( 'charitable_connect_token' );
275
276 // Set the current screen to avoid undefined notices (only when admin is loaded, e.g. admin-ajax.php; not in connect-callback.php).
277 if ( function_exists( 'set_current_screen' ) ) {
278 set_current_screen( 'charitable_page_charitable-settings' );
279 }
280
281 // Prepare variables.
282 $url = esc_url_raw(
283 add_query_arg(
284 array( 'page' => 'charitable-settings' ),
285 admin_url( 'admin.php' )
286 )
287 );
288
289 // Verify pro not already active. Use plugin file + active_plugins, not charitable_is_pro(),
290 // because charitable_is_pro() is true when a valid Pro license is saved (e.g. after Verify),
291 // which would skip install even when the Pro plugin is not installed yet.
292 $pro_plugin_path = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN;
293 $pro_is_active = file_exists( $pro_plugin_path ) && in_array( self::PRO_PLUGIN, (array) get_option( 'active_plugins', array() ), true );
294 if ( $pro_is_active ) {
295 if ( charitable_is_debug() ) {
296 error_log( 'Charitable Connect process: SUCCESS path=pro_already_active' ); // phpcs:ignore
297 }
298 wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'charitable' ) );
299 }
300
301 // Pro plugin file exists but not active: try to activate it, then deactivate Lite.
302 if ( file_exists( $pro_plugin_path ) ) {
303 $active = activate_plugin( self::PRO_PLUGIN, $url, false, true );
304 if ( ! is_wp_error( $active ) ) {
305 if ( charitable_is_debug() ) {
306 error_log( 'Charitable Connect process: SUCCESS path=pro_already_installed_activated' ); // phpcs:ignore
307 }
308 $plugin = plugin_basename( charitable()->get_path() . 'charitable.php' );
309 deactivate_plugins( $plugin );
310 do_action( 'charitable_plugin_deactivated', $plugin );
311 wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'charitable' ) );
312 }
313 }
314
315 // Pro not installed or activation failed: download and install from upgrade server URL.
316 $creds = request_filesystem_credentials( $url, '', false, false );
317
318 // Check for file system permissions.
319 if ( $creds === false || ! WP_Filesystem( $creds ) ) {
320 wp_send_json_error(
321 esc_html__( 'There was an error while installing an upgrade. Please check file system permissions and try again. Also, you can download the plugin from charitable.com and install it manually.', 'charitable' )
322 );
323 }
324
325 /*
326 * We do not need any extra credentials if we have gotten this far, so let's install the plugin.
327 */
328
329 // Do not allow WordPress to search/download translations, as this will break JS output.
330 remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
331
332 // We do not need any extra credentials if we have gotten this far, so let's install the plugin.
333 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
334 require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
335
336 // Create the plugin upgrader with our custom skin.
337 $installer = new Plugin_Upgrader( new Charitable_Skin() );
338
339 // Error check.
340 if ( ! method_exists( $installer, 'install' ) ) {
341 wp_send_json_error( esc_html__( 'No install method found.', 'charitable' ) );
342 }
343
344 // Check license key.
345 $key = get_option( 'charitable_connect', false );
346
347 if ( empty( $key ) ) {
348 wp_send_json_error(
349 new WP_Error(
350 '403',
351 esc_html__( 'No key provided.', 'charitable' )
352 )
353 );
354 }
355
356 $installer->install( $post_url ); // phpcs:ignore
357
358 if ( charitable_is_debug() ) {
359 error_log( 'Charitable Admin Connect process install' ); // phpcs:ignore
360 error_log( $post_url ); // phpcs:ignore
361 error_log( print_r( $installer->plugin_info(), true ) ); // phpcs:ignore
362 }
363
364 // Flush the cache and return the newly installed plugin basename.
365 wp_cache_flush();
366
367 $plugin_basename = $installer->plugin_info();
368
369 if ( $plugin_basename ) {
370
371 // CRITICAL SAFETY CHECK: Verify the newly installed plugin actually exists before deactivating Lite
372 $new_plugin_path = WP_PLUGIN_DIR . '/' . $plugin_basename;
373 if ( ! file_exists( $new_plugin_path ) ) {
374 wp_send_json_error( esc_html__( 'Newly installed plugin not found. Cannot safely deactivate Lite version.', 'charitable' ) );
375 }
376
377 // Deactivate the lite version first.
378 $plugin = plugin_basename( charitable()->get_path( 'plugin-directory' ) . '/charitable/charitable.php' );
379
380 deactivate_plugins( $plugin );
381
382 // phpcs:ignore Charitable.Comments.PHPDocHooks.RequiredHookDocumentation, Charitable.PHP.ValidateHooks.InvalidHookName
383 do_action( 'charitable_plugin_deactivated', $plugin );
384
385 // Activate the plugin silently.
386 $activated = activate_plugin( $plugin_basename, '', false, true );
387
388 if ( ! is_wp_error( $activated ) ) {
389
390 // Add the pro_connect activation date to the activated array.
391 $activated = (array) get_option( 'charitable_activated', array() );
392
393 if ( empty( $activated['pro_connect'] ) ) {
394 $activated['pro_connect'] = time();
395 update_option( 'charitable_activated', $activated );
396 }
397
398 if ( charitable_is_debug() ) {
399 error_log( 'Charitable Admin Connect process activate' ); // phpcs:ignore
400 error_log( 'plugin_basename' ); // phpcs:ignore
401 error_log( $plugin_basename ); // phpcs:ignore
402 error_log( 'activated' ); // phpcs:ignore
403 error_log( print_r( $activated, true ) ); // phpcs:ignore
404 }
405
406 if ( ! empty( $post_key ) && ! empty( $post_item_id ) ) {
407
408 $data = array(
409 'edd_action' => 'activate_license',
410 'license' => $key,
411 'legacy' => false,
412 'item_id' => $post_item_id,
413 'url' => site_url(),
414 );
415
416 if ( charitable_is_debug() ) {
417 error_log( 'Charitable Admin Connect process activate license' ); // phpcs:ignore
418 error_log( print_r( $data, true ) ); // phpcs:ignore
419 }
420
421 $response = wp_remote_post( 'https://wpcharitable.com/edd-api/versions-v2/', array( 'body' => $data ) );
422
423 // Get the body of the response.
424 $body = wp_remote_retrieve_body( $response );
425
426 if ( charitable_is_debug() ) {
427 error_log( 'Charitable Admin Connect process activate license body' ); // phpcs:ignore
428 error_log( $body ); // phpcs:ignore
429 }
430
431 $license_data = json_decode( $body );
432
433 if ( charitable_is_debug() ) {
434 error_log( 'Charitable Admin Connect process activate license data' ); // phpcs:ignore
435 error_log( print_r( $license_data, true ) ); // phpcs:ignore
436 }
437
438 // if $license_day is an object, convert it to an array.
439 if ( is_object( $license_data ) ) {
440 $license_data = (array) $license_data;
441 }
442
443 if ( empty( $license_data ) || is_wp_error( $response ) ) {
444 if ( charitable_is_debug() ) {
445 error_log( 'Charitable Admin Connect process activate license response iniitial failure' ); // phpcs:ignore
446 error_log( print_r( $response, true ) ); // phpcs:ignore
447 error_log( 'Charitable Admin Connect process activate license response iniitial failure license data' ); // phpcs:ignore
448 error_log( print_r( $license_data, true ) ); // phpcs:ignore
449 }
450 } elseif ( ! empty( $license_data['success'] ) && 1 === intval( $license_data['success'] ) && ! empty( $license_data['license'] ) && 'valid' === $license_data['license'] && ! empty( $license_data['expires'] ) ) {
451
452 // Delete transients (related to plugin versions).
453 // delete_transient( '_charitable_plugin_versions' );
454
455 $settings = get_option( 'charitable_settings' );
456
457 $settings['licenses']['charitable-v2'] = array(
458 'license' => $key,
459 'expiration_date' => isset( $license_data['expires'] ) ? $license_data['expires'] : false,
460 'plan_id' => isset( $license_data['price_id'] ) ? $license_data['price_id'] : false,
461 'valid' => true,
462 );
463
464 update_option( 'charitable_settings', $settings );
465
466 if ( charitable_is_debug() ) {
467 error_log( 'Charitable Admin Connect process activate license response good' ); // phpcs:ignore
468 error_log( print_r( $response, true ) ); // phpcs:ignore
469 error_log( 'Charitable Admin Connect process activate license response good license data' ); // phpcs:ignore
470 error_log( print_r( $license_data, true ) ); // phpcs:ignore
471 }
472
473 // Create an empty update transient object instead of null.
474 $empty_transient = new \stdClass();
475 set_site_transient( 'update_plugins', $empty_transient );
476 delete_site_option( 'wpc_plugin_versions' );
477 update_option( 'charitable_connect_complete', true );
478 update_option( 'charitable_connect_completed', true );
479
480 } elseif ( charitable_is_debug() ) {
481 error_log( 'Charitable Admin Connect process activate license response failed' ); // phpcs:ignore
482 error_log( print_r( $response, true ) ); // phpcs:ignore
483 error_log( 'Charitable Admin Connect process activate license response failed license data' ); // phpcs:ignore
484 error_log( print_r( $license_data, true ) ); // phpcs:ignore
485
486 }
487 }
488
489 if ( charitable_is_debug() ) {
490 error_log( 'Charitable Connect process: SUCCESS path=downloaded_and_activated plugin_basename=' . $plugin_basename ); // phpcs:ignore
491 }
492 wp_send_json_success( esc_html__( 'Plugin installed & activated.', 'charitable' ) );
493 } else {
494 // Reactivate the lite plugin if pro activation failed.
495 activate_plugin( plugin_basename( charitable()->get_path() . 'charitable.php' ), '', false, true );
496 wp_send_json_error( esc_html__( 'Pro version installed but needs to be activated on the Plugins page inside your WordPress admin.', 'charitable' ) );
497 }
498 }
499
500 wp_send_json_error( esc_html__( 'No plugin installed.', 'charitable' ) );
501 }
502
503 /**
504 * Get the Charitable Pro download URL from cached API data.
505 *
506 * @since 1.8.9.6
507 * @version 1.8.9.6
508 *
509 * @return string|false The download URL or false if not available.
510 */
511 private function get_pro_download_url() {
512 // Get cached plugin versions data
513 $versions = get_transient( '_charitable_plugin_versions' );
514
515 if ( false === $versions || empty( $versions ) ) {
516 // Try to refresh the data if not cached
517 $addons_directory = Charitable_Addons_Directory::get_instance();
518 $versions = $addons_directory->get_addons_data_from_server();
519
520 if ( false === $versions || empty( $versions ) ) {
521 return false;
522 }
523 }
524
525 // Look for Charitable Pro in the API data. Slug matches EDD product on wpcharitable.com
526 // (versions-v3 returns 'charitable-pro-plugin'; plugin dir is charitable-pro/).
527 $pro_slugs = array( 'charitable-pro-plugin', 'charitable-pro' );
528 if ( is_array( $versions ) ) {
529 foreach ( $versions as $plugin ) {
530 if ( empty( $plugin['slug'] ) || ! in_array( $plugin['slug'], $pro_slugs, true ) ) {
531 continue;
532 }
533 if ( ! empty( $plugin['download_link'] ) && 'missing_license' !== $plugin['download_link'] ) {
534 return $plugin['download_link'];
535 }
536 if ( ! empty( $plugin['install'] ) && 'missing_license' !== $plugin['install'] ) {
537 return $plugin['install'];
538 }
539 if ( ! empty( $plugin['package'] ) && 'missing_license' !== $plugin['package'] ) {
540 return $plugin['package'];
541 }
542 }
543 }
544
545 return false;
546 }
547
548 /**
549 * AJAX handler to download Charitable Pro plugin.
550 *
551 * @since 1.8.9.6
552 * @version 1.8.9.6
553 */
554 public function download_pro_plugin() {
555 // Run a security check.
556 check_ajax_referer( 'charitable-admin', 'nonce' );
557
558 // Check for permissions.
559 if ( ! current_user_can( 'install_plugins' ) ) {
560 wp_send_json_error( array( 'message' => esc_html__( 'You are not allowed to install plugins.', 'charitable' ) ) );
561 }
562
563 $download_url = ! empty( $_POST['download_url'] ) ? esc_url_raw( wp_unslash( $_POST['download_url'] ) ) : '';
564
565 if ( empty( $download_url ) ) {
566 wp_send_json_error(
567 array(
568 'message' => sprintf(
569 esc_html__( 'Download URL missing. Please visit the %1$sAddons page%2$s to download manually.', 'charitable' ),
570 '<a href="' . esc_url( admin_url( 'admin.php?page=charitable-addons' ) ) . '">',
571 '</a>'
572 ),
573 'addons_url' => admin_url( 'admin.php?page=charitable-addons' ),
574 )
575 );
576 }
577
578 // Set the current screen to avoid undefined notices.
579 set_current_screen( 'charitable_page_charitable-settings' );
580
581 // Check file system permissions.
582 $url = admin_url( 'admin.php?page=charitable-settings' );
583 $creds = request_filesystem_credentials( $url, '', false, false );
584
585 if ( $creds === false || ! WP_Filesystem( $creds ) ) {
586 wp_send_json_error(
587 array(
588 'message' => sprintf(
589 esc_html__( 'Unable to access file system. Please visit the %1$sAddons page%2$s to download manually, or see our %3$sinstallation guide%4$s.', 'charitable' ),
590 '<a href="' . esc_url( admin_url( 'admin.php?page=charitable-addons' ) ) . '">',
591 '</a>',
592 '<a href="https://www.wpcharitable.com/documentation/installing-extensions/" target="_blank">',
593 '</a>'
594 ),
595 'addons_url' => admin_url( 'admin.php?page=charitable-addons' ),
596 )
597 );
598 }
599
600 // Do not allow WordPress to search/download translations, as this will break JS output.
601 remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
602
603 // Include required files.
604 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
605 require_once plugin_dir_path( CHARITABLE_DIRECTORY_PATH ) . 'charitable/includes/utilities/Skin.php';
606
607 // Create the plugin upgrader with our custom skin.
608 $installer = new Plugin_Upgrader( new Charitable_Skin() );
609
610 // Error check.
611 if ( ! method_exists( $installer, 'install' ) ) {
612 wp_send_json_error(
613 array(
614 'message' => sprintf(
615 esc_html__( 'Installation method not available. Please visit the %1$sAddons page%2$s to download manually.', 'charitable' ),
616 '<a href="' . esc_url( admin_url( 'admin.php?page=charitable-addons' ) ) . '">',
617 '</a>'
618 ),
619 'addons_url' => admin_url( 'admin.php?page=charitable-addons' ),
620 )
621 );
622 }
623
624 // Install the plugin.
625 $installer->install( $download_url );
626
627 // Flush the cache and return the newly installed plugin basename.
628 wp_cache_flush();
629
630 $plugin_basename = $installer->plugin_info();
631
632 if ( $plugin_basename ) {
633 // Verify the newly installed plugin actually exists before proceeding
634 $new_plugin_path = WP_PLUGIN_DIR . '/' . $plugin_basename;
635 if ( ! file_exists( $new_plugin_path ) ) {
636 wp_send_json_error( array( 'message' => esc_html__( 'Plugin download completed but files not found. Please try again.', 'charitable' ) ) );
637 }
638
639 wp_send_json_success(
640 array(
641 'message' => esc_html__( 'Charitable Pro downloaded successfully!', 'charitable' ),
642 'action' => 'show_activation_confirmation',
643 'plugin_basename' => $plugin_basename,
644 'activation_title' => esc_html__( 'Download Complete!', 'charitable' ),
645 'activation_text' => esc_html__( 'Charitable Pro downloaded successfully! Activate now?', 'charitable' ),
646 )
647 );
648 } else {
649 wp_send_json_error(
650 array(
651 'message' => sprintf(
652 esc_html__( 'Download failed. Please visit the %1$sAddons page%2$s to try again, or see our %3$sinstallation guide%4$s.', 'charitable' ),
653 '<a href="' . esc_url( admin_url( 'admin.php?page=charitable-addons' ) ) . '">',
654 '</a>',
655 '<a href="https://www.wpcharitable.com/documentation/installing-extensions/" target="_blank">',
656 '</a>'
657 ),
658 'addons_url' => admin_url( 'admin.php?page=charitable-addons' ),
659 )
660 );
661 }
662 }
663
664 /**
665 * AJAX handler to activate Charitable Pro plugin.
666 *
667 * @since 1.8.9.6
668 * @version 1.8.9.6
669 */
670 public function activate_pro_plugin() {
671 // Run a security check.
672 check_ajax_referer( 'charitable-admin', 'nonce' );
673
674 // Check for permissions.
675 if ( ! current_user_can( 'activate_plugins' ) ) {
676 wp_send_json_error( array( 'message' => esc_html__( 'You are not allowed to activate plugins.', 'charitable' ) ) );
677 }
678
679 $plugin_basename = ! empty( $_POST['plugin_basename'] ) ? sanitize_text_field( wp_unslash( $_POST['plugin_basename'] ) ) : '';
680
681 if ( empty( $plugin_basename ) ) {
682 wp_send_json_error( array( 'message' => esc_html__( 'Plugin basename missing.', 'charitable' ) ) );
683 }
684
685 // CRITICAL SAFETY CHECK: Verify Pro plugin actually exists before attempting activation
686 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_basename;
687 if ( ! file_exists( $plugin_path ) ) {
688 wp_send_json_error( array( 'message' => esc_html__( 'Plugin files not found. Cannot activate.', 'charitable' ) ) );
689 }
690
691 // Get current plugin info for deactivation.
692 $current_plugin = plugin_basename( charitable()->get_path() . 'charitable.php' );
693
694 // Activate Pro plugin.
695 $activated = activate_plugin( $plugin_basename, '', false, true );
696
697 if ( ! is_wp_error( $activated ) ) {
698 // TODO: Uncomment to deactivate Lite after activating Pro.
699 // deactivate_plugins( $current_plugin );
700 // do_action( 'charitable_plugin_deactivated', $current_plugin );
701
702 $redirect_url = admin_url( 'admin.php?page=charitable-settings&tab=general' );
703 wp_send_json_success(
704 array(
705 'message' => esc_html__( 'Charitable Pro activated successfully!', 'charitable' ),
706 'redirect_url' => $redirect_url,
707 )
708 );
709 } else {
710 wp_send_json_error(
711 array(
712 'message' => esc_html__( 'Charitable Pro installed but activation failed. You can activate it manually from the Plugins page.', 'charitable' ),
713 'error_details' => $activated->get_error_message(),
714 )
715 );
716 }
717 }
718
719 /**
720 * Build redirect data for use after license verify: upgrade_url or show_manual_upgrade (localhost).
721 *
722 * Used so the Verify flow can redirect to upgrade.wpcharitable.com (or show manual message on localhost).
723 *
724 * @since 1.8.9.6
725 *
726 * @param string $license_key License key.
727 * @return array Either ['upgrade_url' => url] or ['show_manual_upgrade' => true, 'url' => ..., 'message' => ...].
728 */
729 public function build_upgrade_redirect_data( $license_key ) {
730 $license_key = trim( $license_key );
731 if ( empty( $license_key ) ) {
732 if ( charitable_is_debug() ) {
733 error_log( 'CHARITABLE DEBUG: build_upgrade_redirect_data empty key' );
734 }
735 return array();
736 }
737 // Pro plugin already active: no redirect (user is on Pro).
738 $pro_plugin_path = WP_PLUGIN_DIR . '/' . self::PRO_PLUGIN;
739 $pro_is_active = file_exists( $pro_plugin_path ) && in_array( self::PRO_PLUGIN, (array) get_option( 'active_plugins', array() ), true );
740 if ( charitable_is_debug() ) {
741 error_log( 'CHARITABLE DEBUG: build_upgrade_redirect_data pro_plugin_path=' . $pro_plugin_path . ' exists=' . ( file_exists( $pro_plugin_path ) ? '1' : '0' ) . ' pro_is_active=' . ( $pro_is_active ? '1' : '0' ) );
742 }
743 if ( $pro_is_active ) {
744 return array();
745 }
746 // Pro installed but inactive: show popup to activate; do not go to upgrade.wpcharitable.com.
747 if ( file_exists( $pro_plugin_path ) ) {
748 return array(
749 'show_activate_pro_popup' => true,
750 'message' => esc_html__( 'Your license is valid. Pro is installed but inactive. Activate now?', 'charitable' ),
751 'redirect_url' => admin_url( 'admin.php?page=charitable-settings&tab=general' ),
752 'plugin_basename' => self::PRO_PLUGIN,
753 );
754 }
755 // Localhost: show manual install message instead of redirect.
756 if ( function_exists( 'charitable_is_localhost' ) && charitable_is_localhost() ) {
757 if ( charitable_is_debug() ) {
758 error_log( 'CHARITABLE DEBUG: build_upgrade_redirect_data returning show_manual_upgrade (localhost)' );
759 }
760 return array(
761 'show_manual_upgrade' => true,
762 'url' => 'https://www.wpcharitable.com/documentation/installing-extensions/',
763 'message' => esc_html__( 'License validated successfully! Since you\'re on localhost, please install Charitable Pro manually using the link below.', 'charitable' ),
764 );
765 }
766 // Set token and connect key so upgrade site callback (process()) can run.
767 $oth_raw = wp_generate_password( 64, true, true );
768 $oth_signed = hash_hmac( 'sha512', $oth_raw, wp_salt() );
769 update_option( 'charitable_connect_token', $oth_raw );
770 update_option( 'charitable_connect', $license_key );
771 $redirect_back = admin_url( 'admin.php?page=charitable-settings&tab=general' );
772 $endpoint = $this->get_connect_callback_endpoint();
773 $upgrade_url = add_query_arg(
774 array(
775 'key' => $license_key,
776 'oth' => $oth_signed,
777 'endpoint' => $endpoint,
778 'version' => charitable()->get_version(),
779 'siteurl' => site_url(),
780 'homeurl' => home_url(),
781 'redirect' => base64_encode( $redirect_back ),
782 ),
783 'https://upgrade.wpcharitable.com/'
784 );
785 if ( charitable_is_debug() ) {
786 error_log( 'CHARITABLE DEBUG: build_upgrade_redirect_data returning upgrade_url (length ' . strlen( $upgrade_url ) . ')' );
787 }
788 return array( 'upgrade_url' => $upgrade_url );
789 }
790
791 /**
792 * Filter callback: add upgrade_url (or show_manual_upgrade) to license verify success response.
793 *
794 * @since 1.8.9.6
795 *
796 * @param array $license_data Data being sent to the frontend.
797 * @param string $license License key.
798 * @return array Modified license_data.
799 */
800 public function add_upgrade_url_to_license_response( $license_data, $license ) {
801 if ( charitable_is_debug() ) {
802 error_log( 'CHARITABLE DEBUG: add_upgrade_url_to_license_response called' );
803 }
804 $extra = $this->build_upgrade_redirect_data( $license );
805 if ( charitable_is_debug() && ! empty( $extra ) ) {
806 error_log( 'CHARITABLE DEBUG: add_upgrade_url_to_license_response adding keys: ' . implode( ', ', array_keys( $extra ) ) );
807 }
808 return array_merge( $license_data, $extra );
809 }
810
811 /**
812 * Get the endpoint URL for the upgrade site callback.
813 *
814 * Uses connect-callback.php in the plugin directory so the callback works when
815 * the site redirects unauthenticated /wp-admin/ requests to the login page.
816 *
817 * @since 1.8.9.6
818 *
819 * @return string URL the upgrade site should POST to.
820 */
821 private function get_connect_callback_endpoint() {
822 $url = charitable()->get_path( 'directory', false ) . 'vendor/wpcharitable/connect-callback.php';
823 return apply_filters( 'charitable_connect_callback_endpoint', $url );
824 }
825
826 /**
827 * Returns and/or create the single instance of this class.
828 *
829 * @since 1.8.1.12
830 *
831 * @return Charitable_Admin_Connect
832 */
833 public static function get_instance() {
834 if ( is_null( self::$instance ) ) {
835 self::$instance = new self();
836 }
837
838 return self::$instance;
839 }
840 }
841