| 1 |
<?php |
| 2 |
/** |
| 3 |
* Charitable Addons Directory. |
| 4 |
* |
| 5 |
* @package Charitable/Classes/Charitable_Addons_Directory |
| 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.0.0 |
| 10 |
* @version 1.8.1.3 |
| 11 |
*/ |
| 12 |
|
| 13 |
// Exit if accessed directly. |
| 14 |
if ( ! defined( 'ABSPATH' ) ) { |
| 15 |
exit; |
| 16 |
} |
| 17 |
|
| 18 |
if ( ! class_exists( 'Charitable_Addons_Directory' ) ) : |
| 19 |
|
| 20 |
/** |
| 21 |
* Charitable_Addons_Directory |
| 22 |
* |
| 23 |
* @final |
| 24 |
* @since 1.0.0 |
| 25 |
*/ |
| 26 |
final class Charitable_Addons_Directory { |
| 27 |
|
| 28 |
/** |
| 29 |
* The update URL. |
| 30 |
* |
| 31 |
* @var string |
| 32 |
*/ |
| 33 |
const UPDATE_URL = 'https://wpcharitable.com'; |
| 34 |
|
| 35 |
/** |
| 36 |
* The single instance of this class. |
| 37 |
* |
| 38 |
* @var Charitable_Addons_Directory|null |
| 39 |
*/ |
| 40 |
private static $instance = null; |
| 41 |
|
| 42 |
/** |
| 43 |
* All the stored licenses. |
| 44 |
* |
| 45 |
* @var array |
| 46 |
*/ |
| 47 |
private $licenses; |
| 48 |
|
| 49 |
/** |
| 50 |
* The key for the failed request cache. |
| 51 |
* |
| 52 |
* @var string |
| 53 |
*/ |
| 54 |
private $failed_request_cache_key = 'wpc_edd_sl_failed_plugin_versions'; |
| 55 |
|
| 56 |
/** |
| 57 |
* Determine if the plugin/addon installations are allowed. |
| 58 |
* |
| 59 |
* @since 1.6.7 |
| 60 |
* |
| 61 |
* @var bool |
| 62 |
*/ |
| 63 |
private $can_install; |
| 64 |
|
| 65 |
/** |
| 66 |
* Create object instance. |
| 67 |
* |
| 68 |
* @since 1.0.0 |
| 69 |
*/ |
| 70 |
private function __construct() { |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Init. |
| 75 |
* |
| 76 |
* @since 1.6.7 |
| 77 |
*/ |
| 78 |
public function init() { |
| 79 |
|
| 80 |
$this->can_install = charitable_can_install( 'addon' ); |
| 81 |
|
| 82 |
do_action( 'charitable_addons_directory_settings_start', $this ); |
| 83 |
|
| 84 |
add_action( 'admin_enqueue_scripts', array( $this, 'charitable_admin_scripts' ) ); |
| 85 |
|
| 86 |
// Add callbacks for content. |
| 87 |
add_action( 'charitable_addons_directory_section', array( $this, 'addons_directory_content' ) ); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Add JS text scripts and load admin scripts. |
| 92 |
* |
| 93 |
* @since 1.8.0 |
| 94 |
* @version 1.8.1.15 - Added upgrade_modal. |
| 95 |
*/ |
| 96 |
public function charitable_admin_scripts() { |
| 97 |
|
| 98 |
$min = charitable_get_min_suffix(); |
| 99 |
$version = charitable()->get_version(); |
| 100 |
|
| 101 |
$assets_dir = charitable()->get_path( 'assets', false ); |
| 102 |
|
| 103 |
$strings = apply_filters( |
| 104 |
'charitable_admin_strings', |
| 105 |
array( |
| 106 |
'nonce' => wp_create_nonce( 'charitable-admin' ), |
| 107 |
'addon_activate' => esc_html__( 'Activate', 'charitable' ), |
| 108 |
'addon_activated' => esc_html__( 'Activated', 'charitable' ), |
| 109 |
'addon_active' => esc_html__( 'Active', 'charitable' ), |
| 110 |
'addon_deactivate' => esc_html__( 'Deactivate', 'charitable' ), |
| 111 |
'addon_inactive' => esc_html__( 'Inactive', 'charitable' ), |
| 112 |
'addon_install' => esc_html__( 'Install Addon', 'charitable' ), |
| 113 |
'addon_error' => sprintf( |
| 114 |
wp_kses( /* translators: %1$s - An addon download URL, %2$s - Link to manual installation guide. */ |
| 115 |
__( 'Could not install the addon. Please <a href="%1$s" target="_blank" rel="noopener noreferrer">download it from wpcharitable.com</a> and <a href="%2$s" target="_blank" rel="noopener noreferrer">install it manually</a>.', 'charitable' ), |
| 116 |
[ |
| 117 |
'a' => [ |
| 118 |
'href' => true, |
| 119 |
'target' => true, |
| 120 |
'rel' => true, |
| 121 |
], |
| 122 |
] |
| 123 |
), |
| 124 |
'https://wpcharitable.com/account/licenses/', |
| 125 |
'https://wpcharitable.com/docs/how-to-manually-install-addons-in-charitable/' |
| 126 |
), |
| 127 |
'plugin_error' => esc_html__( 'Could not install the plugin automatically. Please download and install it manually.', 'charitable' ), |
| 128 |
'addon_search' => esc_html__( 'Searching Addons', 'charitable' ), |
| 129 |
'ajax_url' => admin_url( 'admin-ajax.php' ), |
| 130 |
'admin_url' => admin_url(), |
| 131 |
'ok' => esc_html__( 'OK', 'charitable' ), |
| 132 |
'cancel' => esc_html__( 'Cancel', 'charitable' ), |
| 133 |
'close' => esc_html__( 'Close', 'charitable' ), |
| 134 |
'plugin_install_activate_btn' => esc_html__( 'Install and Activate', 'charitable' ), |
| 135 |
'plugin_install_activate_confirm' => esc_html__( 'needs to be installed and activated to import its forms. Would you like us to install and activate it for you?', 'charitable' ), |
| 136 |
'plugin_activate_btn' => esc_html__( 'Activate', 'charitable' ), |
| 137 |
'plugin_activate_confirm' => esc_html__( 'needs to be activated to import its forms. Would you like us to activate it for you?', 'charitable' ), |
| 138 |
'connecting' => esc_html__( 'Connecting...', 'charitable' ), |
| 139 |
'save_refresh' => esc_html__( 'Save and Refresh', 'charitable' ), |
| 140 |
'server_error' => esc_html__( 'Unfortunately there was a server connection error.', 'charitable' ), |
| 141 |
'testing' => esc_html__( 'Testing', 'charitable' ), |
| 142 |
'upgrade_completed' => esc_html__( 'Upgrade was successfully completed!', 'charitable' ), |
| 143 |
'edit_license' => esc_html__( 'To edit the License Key, please first click the Deactivate Key button. Please note that deactivating this key will remove access to updates, addons, and support.', 'charitable' ), |
| 144 |
'something_went_wrong' => esc_html__( 'Something went wrong', 'charitable' ), |
| 145 |
'success' => esc_html__( 'Success', 'charitable' ), |
| 146 |
'loading' => esc_html__( 'Loading...', 'charitable' ), |
| 147 |
'use_simple_contact_form' => esc_html__( 'Use Simple Contact Form Template', 'charitable' ), |
| 148 |
'error_select_template' => esc_html__( 'Something went wrong while applying the template.', 'charitable' ), |
| 149 |
'plugin_asset_dir' => charitable()->get_path( 'assets', false ), |
| 150 |
'install' => esc_html__( 'Install', 'charitable' ), |
| 151 |
'activate' => esc_html__( 'Activate', 'charitable' ), |
| 152 |
'setup' => esc_html__( 'Setup', 'charitable' ), |
| 153 |
'settings' => esc_html__( 'Settings', 'charitable' ), |
| 154 |
'thanks_for_interest' => esc_html__( 'Thanks for your interest in Charitable Pro!', 'charitable' ), |
| 155 |
'upgrade_modal' => charitable_get_upgrade_modal_text(), |
| 156 |
'autoshow_plugin_notifications' => charitable_get_autoshow_plugin_notifications(), |
| 157 |
'enable_express_checkout_message' => __( 'To enable this payment method, enable Express Checkout', 'charitable' ), |
| 158 |
'activate_now' => esc_html__( 'Activate Now', 'charitable' ), |
| 159 |
'activate_later' => esc_html__( 'Activate Later', 'charitable' ), |
| 160 |
'download_pro' => esc_html__( 'Download Pro', 'charitable' ), |
| 161 |
'downloading' => esc_html__( 'Downloading...', 'charitable' ), |
| 162 |
'downloading_pro_message' => esc_html__( 'Please wait while we download Charitable Pro.', 'charitable' ), |
| 163 |
'activating' => esc_html__( 'Activating...', 'charitable' ), |
| 164 |
'activating_pro_message' => esc_html__( 'Please wait while we activate Charitable Pro.', 'charitable' ), |
| 165 |
'download_failed' => esc_html__( 'Download failed. Please try again.', 'charitable' ), |
| 166 |
'activation_failed' => esc_html__( 'Activation failed. You can activate it manually from the Plugins page.', 'charitable' ), |
| 167 |
'visit_addons_page' => esc_html__( 'Visit Addons Page', 'charitable' ), |
| 168 |
'ready_to_use' => esc_html__( 'Ready to Use', 'charitable' ), |
| 169 |
'pro_ready_to_activate' => esc_html__( 'Charitable Pro has been downloaded and is ready to activate when you\'re ready. You can activate it from the Plugins page.', 'charitable' ), |
| 170 |
'verifying' => esc_html__( 'Verifying...', 'charitable' ), |
| 171 |
'deactivating' => esc_html__( 'Deactivating...', 'charitable' ), |
| 172 |
'manual_install' => esc_html__( 'Manual Install', 'charitable' ), |
| 173 |
'license_validated' => esc_html__( 'License Validated', 'charitable' ), |
| 174 |
) |
| 175 |
); |
| 176 |
|
| 177 |
wp_enqueue_script( |
| 178 |
'listjs', |
| 179 |
$assets_dir . 'js/libraries/list.min.js', |
| 180 |
[ 'jquery' ], |
| 181 |
$version, |
| 182 |
false |
| 183 |
); |
| 184 |
|
| 185 |
wp_register_style( |
| 186 |
'charitable-admin-addons-directory', |
| 187 |
$assets_dir . 'css/admin/charitable-admin-addons-directory' . $min . '.css', |
| 188 |
array(), |
| 189 |
$version |
| 190 |
); |
| 191 |
wp_enqueue_style( 'charitable-admin-addons-directory' ); |
| 192 |
|
| 193 |
wp_register_script( |
| 194 |
'charitable-admin-addon-directory', |
| 195 |
$assets_dir . 'js/admin/charitable-admin-addon-directory' . $min . '.js', |
| 196 |
array( 'jquery' ), |
| 197 |
$version, |
| 198 |
true |
| 199 |
); |
| 200 |
|
| 201 |
wp_enqueue_script( 'charitable-admin-addon-directory' ); |
| 202 |
|
| 203 |
wp_localize_script( |
| 204 |
'charitable-admin-addon-directory', |
| 205 |
'charitable_admin', |
| 206 |
$strings |
| 207 |
); |
| 208 |
} |
| 209 |
|
| 210 |
/** |
| 211 |
* Returns and/or create the single instance of this class. |
| 212 |
* |
| 213 |
* @since 1.2.0 |
| 214 |
* |
| 215 |
* @return Charitable_Addons_Directory |
| 216 |
*/ |
| 217 |
public static function get_instance() { |
| 218 |
if ( is_null( self::$instance ) ) { |
| 219 |
self::$instance = new self(); |
| 220 |
} |
| 221 |
|
| 222 |
return self::$instance; |
| 223 |
} |
| 224 |
|
| 225 |
/** |
| 226 |
* Attemps to confirm if the current license is a legacy one, even if we have a plan_id / slug (which normally came from newer license) |
| 227 |
* |
| 228 |
* @since 1.7.0.4 |
| 229 |
* |
| 230 |
* @return Charitable_Addons_Directory |
| 231 |
*/ |
| 232 |
public static function is_current_plan_legacy() { |
| 233 |
|
| 234 |
$settings = get_option( 'charitable_settings' ); |
| 235 |
|
| 236 |
// always check the v2 settings first. |
| 237 |
$plan_id = isset( $settings['licenses']['charitable-v2']['plan_id'] ) ? intval( $settings['licenses']['charitable-v2']['plan_id'] ) : false; |
| 238 |
$valid = isset( $settings['licenses']['charitable-v2']['valid'] ) ? trim( esc_html( $settings['licenses']['charitable-v2']['valid'] ) ) : false; |
| 239 |
|
| 240 |
if ( false === $valid || '' === $valid ) { |
| 241 |
// try looking at the "legacy" license for a plan_id. |
| 242 |
if ( isset( $settings['licenses']['charitable'] ) ) { |
| 243 |
$plan_id = isset( $settings['licenses']['charitable']['plan_id'] ) ? intval( $settings['licenses']['charitable']['plan_id'] ) : false; |
| 244 |
$valid = isset( $settings['licenses']['charitable']['valid'] ) ? trim( esc_html( $settings['licenses']['charitable']['valid'] ) ) : false; |
| 245 |
|
| 246 |
if ( $plan_id ) { |
| 247 |
return true; |
| 248 |
} |
| 249 |
} |
| 250 |
|
| 251 |
// try looking for an install that just installed an addon license only, which would be another example of a "legacy" license. |
| 252 |
if ( ! empty( $settings['licenses'] ) ) { |
| 253 |
$addon_licenses = $settings['licenses']; |
| 254 |
if ( isset( $addon_licenses['charitable'] ) ) { |
| 255 |
unset( $addon_licenses['charitable'] ); |
| 256 |
} |
| 257 |
if ( isset( $addon_licenses['charitable_pro'] ) ) { |
| 258 |
unset( $addon_licenses['charitable_pro'] ); |
| 259 |
} |
| 260 |
foreach ( $addon_licenses as $addon_license ) { |
| 261 |
if ( isset( $addon_license['valid'] ) && 1 === intval( $addon_license['valid'] ) ) { |
| 262 |
return true; |
| 263 |
} |
| 264 |
} |
| 265 |
} |
| 266 |
} |
| 267 |
|
| 268 |
return false; |
| 269 |
} |
| 270 |
|
| 271 |
/** |
| 272 |
* Returns and/or create the single instance of this class. |
| 273 |
* |
| 274 |
* @since 1.2.0 |
| 275 |
* |
| 276 |
* @return Charitable_Addons_Directory |
| 277 |
*/ |
| 278 |
public static function get_current_plan_slug() { |
| 279 |
|
| 280 |
$settings = get_option( 'charitable_settings' ); |
| 281 |
|
| 282 |
// always check the v2 settings first. |
| 283 |
$plan_id = isset( $settings['licenses']['charitable-v2']['plan_id'] ) ? intval( $settings['licenses']['charitable-v2']['plan_id'] ) : false; |
| 284 |
$valid = isset( $settings['licenses']['charitable-v2']['valid'] ) ? trim( esc_html( $settings['licenses']['charitable-v2']['valid'] ) ) : false; |
| 285 |
|
| 286 |
if ( false === $valid || '' === $valid ) { |
| 287 |
// try looking at the "legacy" license for a plan_id. |
| 288 |
if ( isset( $settings['licenses']['charitable'] ) ) { |
| 289 |
$plan_id = isset( $settings['licenses']['charitable']['plan_id'] ) ? intval( $settings['licenses']['charitable']['plan_id'] ) : false; |
| 290 |
$valid = isset( $settings['licenses']['charitable']['valid'] ) ? trim( esc_html( $settings['licenses']['charitable']['valid'] ) ) : false; |
| 291 |
} |
| 292 |
} |
| 293 |
|
| 294 |
if ( $valid ) { |
| 295 |
switch ( $plan_id ) { |
| 296 |
case 1: |
| 297 |
$plan_slug = 'basic'; |
| 298 |
break; |
| 299 |
case 2: |
| 300 |
$plan_slug = 'plus'; |
| 301 |
break; |
| 302 |
case 3: |
| 303 |
$plan_slug = 'pro'; |
| 304 |
break; |
| 305 |
case 4: |
| 306 |
$plan_slug = 'agency'; |
| 307 |
break; |
| 308 |
default: |
| 309 |
$plan_slug = 'lite'; |
| 310 |
break; |
| 311 |
} |
| 312 |
} else { |
| 313 |
$plan_slug = 'lite'; |
| 314 |
} |
| 315 |
|
| 316 |
return $plan_slug; |
| 317 |
} |
| 318 |
|
| 319 |
/** |
| 320 |
* Callback for displaying the UI for Addons. |
| 321 |
* |
| 322 |
* @since 1.7.0 |
| 323 |
* @version 1.8.8.6 |
| 324 |
*/ |
| 325 |
public function addons_directory_content() { |
| 326 |
|
| 327 |
// Get Addons. |
| 328 |
$temp_addons = $this->get_addons(); |
| 329 |
|
| 330 |
// If error(s) occured during license key verification, display them and exit now. |
| 331 |
if ( empty( $temp_addons ) ) { ?> |
| 332 |
|
| 333 |
<h1 class="page-title"><?php esc_html_e( 'Charitable Addons', 'charitable' ); ?></h1> |
| 334 |
|
| 335 |
<div class="error below-h2"> |
| 336 |
<p> |
| 337 |
<?php esc_html_e( 'There are some communication or license issues. Please make sure your server is able to communicate with wpcharitable.com. Contact Charitable Support for additional assistance.', 'charitable' ); ?> |
| 338 |
</p> |
| 339 |
</div> |
| 340 |
<?php |
| 341 |
|
| 342 |
if ( charitable_is_debug() ) { // @codingStandardsIgnoreLine |
| 343 |
error_log( 'addons_directory_content - communication issue' ); // @codingStandardsIgnoreLine |
| 344 |
} |
| 345 |
|
| 346 |
return; |
| 347 |
} |
| 348 |
|
| 349 |
$recommended_addons = array_map( array( $this, 'prepare_addon_data' ), ! empty( $temp_addons['recommended'] ) ? $temp_addons['recommended'] : array() ); |
| 350 |
$unlicensed_addons = array_map( array( $this, 'prepare_addon_data' ), ! empty( $temp_addons['unlicensed'] ) ? $temp_addons['unlicensed'] : array() ); |
| 351 |
$licensed_addons = array_map( array( $this, 'prepare_addon_data' ), ! empty( $temp_addons['licensed'] ) ? $temp_addons['licensed'] : array() ); |
| 352 |
|
| 353 |
$installed_plugins = get_plugins(); |
| 354 |
|
| 355 |
// Categorize addons. |
| 356 |
$featured_addons = array(); |
| 357 |
$installed_addons = array(); |
| 358 |
$available_addons = array(); |
| 359 |
$premium_addons = array(); |
| 360 |
$all_addons_display = array(); |
| 361 |
|
| 362 |
// Combine all addons into a single array to avoid duplicates. |
| 363 |
$all_api_addons = array_merge( $recommended_addons, $licensed_addons, $unlicensed_addons ); |
| 364 |
$all_addons = array(); |
| 365 |
foreach ( $all_api_addons as $addon ) { |
| 366 |
if ( isset( $addon['slug'] ) ) { |
| 367 |
$all_addons[ $addon['slug'] ] = $addon; |
| 368 |
} |
| 369 |
} |
| 370 |
|
| 371 |
foreach ( $all_addons as $addon ) { |
| 372 |
// Apply slug replacements for plugin detection (same logic as in prepare_addon_data). |
| 373 |
$replacements = array( |
| 374 |
'charitable-recurring-donations' => 'charitable-recurring', |
| 375 |
'charitable-anonymous-donations' => 'charitable-anonymous', |
| 376 |
'charitable-easy-digital-downloads-connect' => 'charitable-edd', |
| 377 |
); |
| 378 |
|
| 379 |
$slug_for_detection = $addon['slug']; |
| 380 |
if ( array_key_exists( $addon['slug'], $replacements ) ) { |
| 381 |
$slug_for_detection = $replacements[ $addon['slug'] ]; |
| 382 |
} |
| 383 |
|
| 384 |
$plugin_basename = $this->get_plugin_basename_from_slug( $slug_for_detection ); |
| 385 |
$is_installed = array_key_exists( $plugin_basename, $installed_plugins ); |
| 386 |
$is_featured = ! empty( $addon['featured'] ) && in_array( 'recommended', $addon['featured'], true ); |
| 387 |
$is_available = ! empty( $addon['license'] ) && in_array( $this->get_current_plan_slug(), $addon['license'], true ); |
| 388 |
$addon['is_updated'] = $this->is_addon_updated( $addon ); |
| 389 |
$addon['is_new'] = $this->is_addon_new( $addon ); |
| 390 |
$addon['is_active'] = is_plugin_active( $plugin_basename ); |
| 391 |
|
| 392 |
// Check if addon is included in pro. |
| 393 |
$addons_included_in_pro = array( |
| 394 |
'charitable-videos', |
| 395 |
'charitable-pdf-receipts', |
| 396 |
'charitable-stripe', |
| 397 |
'charitable-geolocation', |
| 398 |
'charitable-donor-comments', |
| 399 |
); |
| 400 |
$is_included_in_pro = in_array( $addon['slug'], $addons_included_in_pro, true ); |
| 401 |
$addon['is_included_in_pro'] = $is_included_in_pro; |
| 402 |
|
| 403 |
// Prioritize installed addons over featured status. |
| 404 |
// Don't add addons included in pro to the installed category. |
| 405 |
if ( $is_installed && ! $is_included_in_pro ) { |
| 406 |
$installed_addons[] = $addon; |
| 407 |
} elseif ( $is_featured ) { |
| 408 |
$featured_addons[] = $addon; |
| 409 |
} elseif ( $is_available && ! $is_included_in_pro ) { |
| 410 |
$available_addons[] = $addon; |
| 411 |
} elseif ( ! $is_included_in_pro ) { |
| 412 |
$premium_addons[] = $addon; |
| 413 |
} |
| 414 |
} |
| 415 |
|
| 416 |
$addon_categories = array( |
| 417 |
'featured' => array( |
| 418 |
'label' => __( 'Featured', 'charitable' ), |
| 419 |
'addons' => $featured_addons, |
| 420 |
), |
| 421 |
'installed' => array( |
| 422 |
'label' => __( 'Installed', 'charitable' ), |
| 423 |
'addons' => $installed_addons, |
| 424 |
), |
| 425 |
'available' => array( |
| 426 |
'label' => __( 'Available In Your Plan', 'charitable' ), |
| 427 |
'addons' => $available_addons, |
| 428 |
), |
| 429 |
'premium' => array( |
| 430 |
'label' => __( 'Premium Addons', 'charitable' ), |
| 431 |
'addons' => $premium_addons, |
| 432 |
), |
| 433 |
); |
| 434 |
|
| 435 |
// If no Addon(s) were returned, our API call returned an error. |
| 436 |
// Show an error message with a button to reload the page, which will trigger another API call. |
| 437 |
if ( ! $all_addons ) { |
| 438 |
?> |
| 439 |
<form id="charitable-addons-refresh-addons-form" method="post"> |
| 440 |
<p> |
| 441 |
<?php esc_html_e( 'There was an issue retrieving the addons for this site. Please click on the button below the refresh the addons data.', 'charitable' ); ?> |
| 442 |
</p> |
| 443 |
<p> |
| 444 |
<a href="<?php echo esc_url( wp_unslash( $_SERVER['REQUEST_URI'] ) ); ?>" class="button button-primary"><?php esc_html_e( 'Refresh Addons', 'charitable' ); ?></a> <?php // @codingStandardsIgnoreLine ?> |
| 445 |
</p> |
| 446 |
</form> |
| 447 |
<?php |
| 448 |
return; |
| 449 |
} |
| 450 |
|
| 451 |
// If here, we have Addons to display, so let's output them now. |
| 452 |
// Get installed plugins and upgrade URL. |
| 453 |
$upgrade_url = $this->charitable_get_upgrade_link(); |
| 454 |
$type = ( isset( $_GET['plan'] ) ) ? esc_attr( $_GET['plan'] ) : $this->get_current_plan_slug(); // phpcs:ignore |
| 455 |
$preset_search = ( isset( $_GET['search'] ) ) ? trim( esc_attr( $_GET['search'] ) ) : ''; // phpcs:ignore |
| 456 |
?> |
| 457 |
|
| 458 |
<div class="charitable-addons-header"> |
| 459 |
<h1 class="charitable-addons-header-title"> |
| 460 |
Charitable Addons |
| 461 |
<a href="<?php echo esc_url( add_query_arg( 'charitable_refresh_addons', '1' ) ); ?>" class="charitable-addons-link"> |
| 462 |
<?php esc_html_e( 'Refresh Addons', 'charitable' ); ?> |
| 463 |
</a> |
| 464 |
</h1> |
| 465 |
<div class="charitable-addons-header-search"> |
| 466 |
<input type="search" placeholder="<?php esc_html_e( 'Search Addons', 'charitable' ); ?>" id="charitable-admin-addons-search" value="<?php echo esc_html( $preset_search ); ?>" /> |
| 467 |
</div> |
| 468 |
</div> |
| 469 |
<?php |
| 470 |
|
| 471 |
// phpcs:disable |
| 472 |
if ( defined( 'CHARITABLE_DEBUG_LICENSE' ) && CHARITABLE_DEBUG_LICENSE ) { |
| 473 |
echo '<p>Plan type is: ' . $type . '</p>'; |
| 474 |
if ( charitable_is_debug() ) { |
| 475 |
error_log( 'addons_directory_content' ); |
| 476 |
error_log( print_r( $type, true ) ); |
| 477 |
error_log( print_r( $installed_plugins, true ) ); |
| 478 |
error_log( print_r( $upgrade_url, true ) ); |
| 479 |
} |
| 480 |
} |
| 481 |
// phpcs:enable |
| 482 |
|
| 483 |
/* |
| 484 |
* Pro addon card visibility in the Charitable → Addons screen. |
| 485 |
* |
| 486 |
* Default: both `charitable-pro-plugin` and `charitable-pro` slugs are hidden |
| 487 |
* so the normal upgrade path (wpcharitable.com checkout → Plugins → Add New |
| 488 |
* upload) is the only way to install Pro. |
| 489 |
* |
| 490 |
* With CHARITABLE_SHOW_PRO_IN_ADDONS defined truthy (wp-config.php or a |
| 491 |
* code-snippet plugin), the Pro card is exposed so Pro can be installed |
| 492 |
* server-to-server from this screen — unblocking managed-host customers |
| 493 |
* (WP.com, Kinsta, WP Engine, SiteGround) whose manual-upload path rejects |
| 494 |
* the Pro zip with `http_error`. |
| 495 |
* |
| 496 |
* The wpcharitable.com addons API currently returns Pro under the legacy slug |
| 497 |
* `charitable-pro-plugin`, while on disk Pro lives at `charitable-pro/ |
| 498 |
* charitable-pro.php`. If we rendered the raw entry, install-state detection, |
| 499 |
* basename lookup, and activation would all use the wrong path and the flow |
| 500 |
* would fail at activation. So when we expose Pro we NORMALISE the entry in |
| 501 |
* place: rewrite its slug to `charitable-pro` and its path to match the |
| 502 |
* on-disk location. |
| 503 |
* |
| 504 |
* Wrapped in try/catch so that any unexpected payload shape cannot fatal the |
| 505 |
* admin Addons screen. On failure we fall back to the safe pre-1.8.10.5 |
| 506 |
* behaviour of hiding both Pro slugs. |
| 507 |
* |
| 508 |
* @since 1.8.5 |
| 509 |
*/ |
| 510 |
try { |
| 511 |
$show_pro = defined( 'CHARITABLE_SHOW_PRO_IN_ADDONS' ) && CHARITABLE_SHOW_PRO_IN_ADDONS; |
| 512 |
|
| 513 |
foreach ( $addon_categories as $slug => &$category ) { |
| 514 |
if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) { |
| 515 |
continue; |
| 516 |
} |
| 517 |
foreach ( $category['addons'] as $k => &$v ) { |
| 518 |
if ( ! is_array( $v ) || ! isset( $v['slug'] ) ) { |
| 519 |
continue; |
| 520 |
} |
| 521 |
|
| 522 |
$is_pro_legacy = 'charitable-pro-plugin' === $v['slug']; |
| 523 |
$is_pro_canonical = 'charitable-pro' === $v['slug']; |
| 524 |
|
| 525 |
if ( ! $is_pro_legacy && ! $is_pro_canonical ) { |
| 526 |
continue; |
| 527 |
} |
| 528 |
|
| 529 |
if ( ! $show_pro ) { |
| 530 |
unset( $category['addons'][ $k ] ); |
| 531 |
continue; |
| 532 |
} |
| 533 |
|
| 534 |
// Normalise the legacy slug onto the canonical on-disk layout so |
| 535 |
// install, activation, and "is installed" detection all line up. |
| 536 |
if ( $is_pro_legacy ) { |
| 537 |
$v['slug'] = 'charitable-pro'; |
| 538 |
$v['path'] = 'charitable-pro/charitable-pro.php'; |
| 539 |
} |
| 540 |
} |
| 541 |
// Reindex array after any removal. |
| 542 |
$category['addons'] = array_values( $category['addons'] ); |
| 543 |
} |
| 544 |
unset( $category ); // Break the reference. |
| 545 |
unset( $v ); // Break the reference. |
| 546 |
|
| 547 |
// Defensive dedup: if both the legacy and canonical slugs were present |
| 548 |
// server-side, the rename above could produce two `charitable-pro` |
| 549 |
// entries. Keep only the first across all categories. |
| 550 |
if ( $show_pro ) { |
| 551 |
$seen_pro = false; |
| 552 |
foreach ( $addon_categories as $slug => &$category ) { |
| 553 |
if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) { |
| 554 |
continue; |
| 555 |
} |
| 556 |
foreach ( $category['addons'] as $k => &$v ) { |
| 557 |
if ( is_array( $v ) && isset( $v['slug'] ) && 'charitable-pro' === $v['slug'] ) { |
| 558 |
if ( $seen_pro ) { |
| 559 |
unset( $category['addons'][ $k ] ); |
| 560 |
} else { |
| 561 |
$seen_pro = true; |
| 562 |
} |
| 563 |
} |
| 564 |
} |
| 565 |
$category['addons'] = array_values( $category['addons'] ); |
| 566 |
} |
| 567 |
unset( $category ); |
| 568 |
unset( $v ); |
| 569 |
} |
| 570 |
} catch ( \Throwable $e ) { |
| 571 |
// Fall back to the pre-1.8.10.5 behaviour: always hide both Pro slugs. |
| 572 |
$addons_to_remove = array( 'charitable-pro-plugin', 'charitable-pro' ); |
| 573 |
if ( is_array( $addon_categories ) ) { |
| 574 |
foreach ( $addon_categories as $slug => &$category ) { |
| 575 |
if ( ! is_array( $category ) || ! isset( $category['addons'] ) || ! is_array( $category['addons'] ) ) { |
| 576 |
continue; |
| 577 |
} |
| 578 |
foreach ( $category['addons'] as $k => &$v ) { |
| 579 |
if ( is_array( $v ) && isset( $v['slug'] ) && in_array( $v['slug'], $addons_to_remove, true ) ) { |
| 580 |
unset( $category['addons'][ $k ] ); |
| 581 |
} |
| 582 |
} |
| 583 |
$category['addons'] = array_values( $category['addons'] ); |
| 584 |
} |
| 585 |
unset( $category ); |
| 586 |
unset( $v ); |
| 587 |
} |
| 588 |
if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) { |
| 589 |
error_log( 'CHARITABLE: Pro addon card gating failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 590 |
} |
| 591 |
} |
| 592 |
?> |
| 593 |
|
| 594 |
<div id="charitable-addons"> |
| 595 |
<?php foreach ( $addon_categories as $slug => $category ) : ?> |
| 596 |
<?php if ( ! empty( $category['addons'] ) ) : ?> |
| 597 |
<div class="charitable-addons-section"> |
| 598 |
<?php |
| 599 |
$is_featured_section = ( 'featured' === $slug ); |
| 600 |
$header_classes = array( 'charitable-addons-section-header' ); |
| 601 |
if ( $is_featured_section ) { |
| 602 |
$header_classes[] = 'charitable-addons-section-header-featured'; |
| 603 |
} |
| 604 |
?> |
| 605 |
<div class="<?php echo esc_attr( implode( ' ', $header_classes ) ); ?>" data-section="<?php echo esc_attr( $slug ); ?>"> |
| 606 |
<div class="charitable-addons-section-title"> |
| 607 |
<?php echo esc_html( $category['label'] ); ?> |
| 608 |
<?php |
| 609 |
if ( 'available' === $slug ) { |
| 610 |
$plan_slug = $this->get_current_plan_slug(); |
| 611 |
if ( ! empty( $plan_slug ) && 'lite' !== $plan_slug ) { |
| 612 |
echo ' <span class="charitable-badge charitable-badge-lg charitable-badge-inline charitable-badge-titanium charitable-badge-rounded">' . esc_html( $plan_slug ) . '</span>'; |
| 613 |
} |
| 614 |
} |
| 615 |
?> |
| 616 |
</div> |
| 617 |
<?php if ( ! $is_featured_section ) : ?> |
| 618 |
<div class="charitable-addons-section-toggle"> |
| 619 |
<span class="dashicons dashicons-arrow-right-alt2"></span> |
| 620 |
</div> |
| 621 |
<?php endif; ?> |
| 622 |
</div> |
| 623 |
<div id="charitable-addons-<?php echo esc_attr( $slug ); ?>" class="charitable-addons-list charitable-addons-section-content"<?php echo ( $is_featured_section ) ? '' : ' style="' . esc_attr( 'display: none;' ) . '"'; ?>> |
| 624 |
<?php |
| 625 |
foreach ( (array) $category['addons'] as $i => $addon ) { |
| 626 |
$this->get_addon_card( $addon, $i, true, $installed_plugins ); |
| 627 |
} |
| 628 |
?> |
| 629 |
</div> |
| 630 |
</div> |
| 631 |
<?php endif; ?> |
| 632 |
<?php endforeach; ?> |
| 633 |
</div> |
| 634 |
<?php |
| 635 |
} |
| 636 |
|
| 637 |
/** |
| 638 |
* Retrieves addons from the stored transient or remote server. |
| 639 |
* |
| 640 |
* @since 1.7.0 |
| 641 |
* |
| 642 |
* @return bool | array false | Array of licensed and unlicensed Addons. |
| 643 |
*/ |
| 644 |
public function get_addons() { |
| 645 |
|
| 646 |
// Get license key and type. |
| 647 |
$key = false; // charitable_get_license_key();. |
| 648 |
$type = $this->get_current_plan_slug(); |
| 649 |
$addons = get_transient( '_charitable_addons' ); // @codingStandardsIgnoreLine - testing. |
| 650 |
|
| 651 |
// Get addons data from transient or perform API query if no transient. |
| 652 |
if ( false === $addons ) { |
| 653 |
$addons = $this->get_addons_data_from_server( $key ); |
| 654 |
} |
| 655 |
|
| 656 |
// If no Addons exist, return false. |
| 657 |
if ( ! $addons ) { |
| 658 |
return array(); |
| 659 |
} |
| 660 |
|
| 661 |
// Iterate through Addons, to build two arrays: |
| 662 |
// - Addons the user is licensed to use, |
| 663 |
// - Addons the user isn't licensed to use. |
| 664 |
$results = array( |
| 665 |
'recommended' => array(), |
| 666 |
'licensed' => array(), |
| 667 |
'unlicensed' => array(), |
| 668 |
); |
| 669 |
|
| 670 |
foreach ( (array) $addons as $i => $addon ) { |
| 671 |
|
| 672 |
if ( ! isset( $addon['slug'] ) || $addon['slug'] === '' || strtolower( $addon['slug'] ) === 'auto draft' ) { |
| 673 |
continue; |
| 674 |
} |
| 675 |
|
| 676 |
if ( ! is_array( $addon ) ) { |
| 677 |
$addon = array(); |
| 678 |
} |
| 679 |
|
| 680 |
$addon['path'] = ( '' === $addon['path'] ) ? sprintf( '%1$s/%1$s.php', $addon['slug'] ) : $addon['path']; |
| 681 |
|
| 682 |
$type = ( isset( $_GET['license'] ) ) ? $_GET['license'] : $this->get_current_plan_slug(); // phpcs:ignore |
| 683 |
|
| 684 |
if ( isset( $addon['featured'] ) && in_array( 'recommended', $addon['featured'] ) ) { // phpcs:ignore |
| 685 |
$results['recommended'][] = (array) $addon; |
| 686 |
continue; |
| 687 |
} else { // phpcs:ignore |
| 688 |
// Determine if the addon belongs licensed or unlicensed based on the confirmed plan. |
| 689 |
if ( isset( $addon['license'] ) && in_array( $type, $addon['license'] ) ) { // phpcs:ignore |
| 690 |
$results['licensed'][] = (array) $addon; |
| 691 |
continue; |
| 692 |
} else { |
| 693 |
$results['unlicensed'][] = (array) $addon; |
| 694 |
continue; |
| 695 |
} |
| 696 |
} |
| 697 |
} |
| 698 |
|
| 699 |
// Return Addons, split by licensed and unlicensed. |
| 700 |
return $results; |
| 701 |
} |
| 702 |
|
| 703 |
|
| 704 |
/** |
| 705 |
* Stores an upgrade link used for the addons page when a person needs a pro license. |
| 706 |
* |
| 707 |
* @since 1.7.0.4 |
| 708 |
* |
| 709 |
* @return string The url |
| 710 |
*/ |
| 711 |
public function charitable_get_upgrade_link() { |
| 712 |
return 'https://wpcharitable.com/lite-upgrade/?discount=LITEUPGRADE&utm_source=WordPress&utm_campaign=liteplugin&utm_medium=addons&utm_content=ActiveCampaign%20Addon'; |
| 713 |
} |
| 714 |
|
| 715 |
/** |
| 716 |
* Retrieve the plugin basename from the plugin slug. |
| 717 |
* |
| 718 |
* @since 1.7.0 |
| 719 |
* |
| 720 |
* @param string $slug The plugin slug. |
| 721 |
* @return string The plugin basename if found, else the plugin slug. |
| 722 |
*/ |
| 723 |
public function get_plugin_basename_from_slug( $slug ) { |
| 724 |
|
| 725 |
$keys = array_keys( get_plugins() ); |
| 726 |
|
| 727 |
foreach ( $keys as $key ) { |
| 728 |
if ( preg_match( '|^' . $slug . '|', $key ) ) { |
| 729 |
return $key; |
| 730 |
} |
| 731 |
} |
| 732 |
|
| 733 |
return $slug; |
| 734 |
} |
| 735 |
|
| 736 |
/** |
| 737 |
* Outputs the addon "box" on the addons page. |
| 738 |
* |
| 739 |
* @since 1.7.0 |
| 740 |
* @version 1.8.1.12 - Added EDD to replacements string check. |
| 741 |
* |
| 742 |
* @param object $addon Addon data from the API / transient call. |
| 743 |
* @param int $counter Index of this Addon in the collection. |
| 744 |
* @param bool $is_licensed Whether the Addon is licensed for use. |
| 745 |
* @param array $installed_plugins Installed WordPress Plugins. |
| 746 |
*/ |
| 747 |
public function get_addon_card( $addon, $counter = 0, $is_licensed = false, $installed_plugins = false ) { |
| 748 |
|
| 749 |
if ( ! isset( $addon['slug'] ) ) { |
| 750 |
return; |
| 751 |
} |
| 752 |
|
| 753 |
// Setup some vars. |
| 754 |
$plugin_basename = $this->get_plugin_basename_from_slug( $addon['slug'] ); |
| 755 |
$replacements = array( |
| 756 |
'charitable-recurring-donations' => 'charitable-recurring', |
| 757 |
'charitable-anonymous-donations' => 'charitable-anonymous', |
| 758 |
'charitable-easy-digital-downloads-connect' => 'charitable-edd', |
| 759 |
); |
| 760 |
|
| 761 |
if ( array_key_exists( $addon['slug'], $replacements ) ) { |
| 762 |
$plugin_basename = $this->get_plugin_basename_from_slug( $replacements[ $addon['slug'] ] ); |
| 763 |
} |
| 764 |
|
| 765 |
if ( ! $installed_plugins ) { |
| 766 |
$installed_plugins = get_plugins(); |
| 767 |
} |
| 768 |
|
| 769 |
// If the Addon doesn't supply an upgrade_url key, it's because the user hasn't provided a license. |
| 770 |
// get_upgrade_link() will return the Lite or Pro link as necessary for us. |
| 771 |
if ( ! isset( $addon['upgrade_url'] ) ) { |
| 772 |
$addon['upgrade_url'] = $this->charitable_get_upgrade_link(); |
| 773 |
} |
| 774 |
|
| 775 |
// Add marketing tracking. |
| 776 |
$addon['upgrade_url'] = add_query_arg( |
| 777 |
array( |
| 778 |
'utm_source' => 'proplugin', |
| 779 |
'utm_medium' => 'addonspage', |
| 780 |
'utm_campaign' => str_replace( '-', '', $addon['slug'] ) . 'addon', |
| 781 |
), |
| 782 |
$addon['upgrade_url'] |
| 783 |
); |
| 784 |
|
| 785 |
switch ( $this->get_current_plan_slug() ) { |
| 786 |
case 'basic': |
| 787 |
$the_plans = array( 'basic' ); |
| 788 |
break; |
| 789 |
case 'plus': |
| 790 |
$the_plans = array( 'basic', 'plus' ); |
| 791 |
break; |
| 792 |
case 'pro': |
| 793 |
$the_plans = array( 'basic', 'plus', 'pro' ); |
| 794 |
break; |
| 795 |
case 'agency': |
| 796 |
$the_plans = array( 'basic', 'plus', 'pro', 'agency', 'elite' ); |
| 797 |
break; |
| 798 |
|
| 799 |
default: |
| 800 |
$the_plans = array(); |
| 801 |
break; |
| 802 |
} |
| 803 |
|
| 804 |
if ( ! isset( $installed_plugins[ $plugin_basename ] ) ) { |
| 805 |
|
| 806 |
$addon['status'] = 'missing'; |
| 807 |
$addon['action'] = 'license'; |
| 808 |
|
| 809 |
foreach ( $addon['license'] as $license_to_check ) { |
| 810 |
if ( in_array( $license_to_check, $the_plans ) ) { // phpcs:ignore |
| 811 |
$addon['status'] = 'missing'; |
| 812 |
$addon['action'] = 'install'; |
| 813 |
} |
| 814 |
} |
| 815 |
|
| 816 |
/* |
| 817 |
* Charitable Pro is available to every legitimate license tier (Basic, |
| 818 |
* Plus, Pro, Agency). The tier-match loop above would otherwise show |
| 819 |
* "Upgrade Now" to Basic/Plus customers because Pro's API license field |
| 820 |
* typically only lists `pro`. Override to "install" when the current |
| 821 |
* site has any valid license AND the API actually returned a usable |
| 822 |
* download_link — without a download_link the install step would fail, |
| 823 |
* so we fall back to the upgrade CTA in that case. |
| 824 |
* |
| 825 |
* Wrapped in try/catch so that a failure in the licence helper chain |
| 826 |
* (registry / vendor licence lookup) cannot fatal an admin page render. |
| 827 |
* On failure the card simply keeps whatever action the tier-match loop |
| 828 |
* above assigned — i.e. the original pre-1.8.10.5 behaviour. |
| 829 |
*/ |
| 830 |
try { |
| 831 |
if ( 'charitable-pro' === $addon['slug'] |
| 832 |
&& 'install' !== $addon['action'] |
| 833 |
&& function_exists( 'charitable_is_pro' ) |
| 834 |
&& charitable_is_pro() |
| 835 |
&& ! empty( $addon['download_link'] ) ) { |
| 836 |
$addon['status'] = 'missing'; |
| 837 |
$addon['action'] = 'install'; |
| 838 |
} |
| 839 |
} catch ( \Throwable $e ) { |
| 840 |
if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) { |
| 841 |
error_log( 'CHARITABLE: Pro install-action override failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 842 |
} |
| 843 |
} |
| 844 |
} else { // phpcs:ignore |
| 845 |
// Plugin is installed. |
| 846 |
if ( $addon['is_active'] ) { |
| 847 |
$addon['status'] = 'active'; |
| 848 |
$addon['action'] = 'deactivate'; |
| 849 |
} else { |
| 850 |
$addon['status'] = 'installed'; |
| 851 |
$addon['action'] = 'activate'; |
| 852 |
} |
| 853 |
} |
| 854 |
|
| 855 |
// Check if addon is included in pro. |
| 856 |
$addons_included_in_pro = array( |
| 857 |
'charitable-videos', |
| 858 |
'charitable-pdf-receipts', |
| 859 |
'charitable-stripe', |
| 860 |
'charitable-geolocation', |
| 861 |
'charitable-donor-comments', |
| 862 |
); |
| 863 |
$is_included_in_pro = in_array( $addon['slug'], $addons_included_in_pro, true ); |
| 864 |
|
| 865 |
// Check if addon is installed. |
| 866 |
$is_installed = array_key_exists( $plugin_basename, $installed_plugins ); |
| 867 |
|
| 868 |
// Override status for addons included in pro. |
| 869 |
if ( $is_included_in_pro ) { |
| 870 |
$addon['status'] = 'included_in_pro'; |
| 871 |
$addon['action'] = 'none'; |
| 872 |
} |
| 873 |
|
| 874 |
// Override status for installed addons to show "Installed" text instead of button. |
| 875 |
if ( $is_installed && ! $is_included_in_pro ) { |
| 876 |
$addon['status'] = 'installed_text'; |
| 877 |
$addon['action'] = 'none'; |
| 878 |
} |
| 879 |
|
| 880 |
$button = $this->get_addon_button_html( $addon ); |
| 881 |
|
| 882 |
$status_label = $this->get_addon_status_label( $addon['status'] ); |
| 883 |
|
| 884 |
// Resolve the icon. Lookup order: |
| 885 |
// 1. Local SVG (assets/images/addons/addon-icon-{slug}.svg) |
| 886 |
// 2. Local PNG (assets/images/addons/addon-icon-{slug}.png) |
| 887 |
// 3. API-supplied icon URL (so newly added addons show the right art |
| 888 |
// even before a local asset ships in a Lite release) |
| 889 |
// 4. Stripe placeholder (last resort so the card never renders broken) |
| 890 |
$icon_file_name = str_replace( array( 'charitable-', '-banner' ), '', $addon['slug'] ); |
| 891 |
$assets_path_fs = charitable()->get_path( 'assets', true ) . 'images/addons/'; |
| 892 |
$assets_path_url = charitable()->get_path( 'assets', false ) . 'images/addons/'; |
| 893 |
$placeholder_icon = $assets_path_url . 'addon-icon-stripe.png'; |
| 894 |
|
| 895 |
if ( file_exists( $assets_path_fs . 'addon-icon-' . $icon_file_name . '.svg' ) ) { |
| 896 |
$icon = $assets_path_url . 'addon-icon-' . $icon_file_name . '.svg'; |
| 897 |
} elseif ( file_exists( $assets_path_fs . 'addon-icon-' . $icon_file_name . '.png' ) ) { |
| 898 |
$icon = $assets_path_url . 'addon-icon-' . $icon_file_name . '.png'; |
| 899 |
} elseif ( ! empty( $addon['icon'] ) && filter_var( $addon['icon'], FILTER_VALIDATE_URL ) ) { |
| 900 |
$icon = esc_url( $addon['icon'] ); |
| 901 |
} else { |
| 902 |
$icon = $placeholder_icon; |
| 903 |
} |
| 904 |
|
| 905 |
// get the plugin description. |
| 906 |
// Guard against a missing / malformed `sections` entry from the API so a |
| 907 |
// single bad addon row can't blow up the whole directory render. Belt + |
| 908 |
// suspenders: the conditions below handle normal failure modes, and the |
| 909 |
// try/catch handles anything pathological. |
| 910 |
$sections = array(); |
| 911 |
$description = ''; |
| 912 |
try { |
| 913 |
if ( isset( $addon['sections'] ) && is_string( $addon['sections'] ) && '' !== $addon['sections'] ) { |
| 914 |
$maybe_sections = @unserialize( $addon['sections'], array( 'allowed_classes' => false ) ); // phpcs:ignore |
| 915 |
if ( is_array( $maybe_sections ) ) { |
| 916 |
$sections = $maybe_sections; |
| 917 |
} |
| 918 |
} |
| 919 |
$description = wp_strip_all_tags( isset( $sections['description'] ) ? (string) $sections['description'] : '' ); |
| 920 |
} catch ( \Throwable $e ) { |
| 921 |
$description = ''; |
| 922 |
if ( function_exists( 'charitable_is_debug' ) && charitable_is_debug() ) { |
| 923 |
error_log( 'CHARITABLE: Addon description parse failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log |
| 924 |
} |
| 925 |
} |
| 926 |
|
| 927 |
// The wpcharitable.com addons API currently returns an empty description for |
| 928 |
// the Charitable Pro entry. Provide a hard-coded fallback so the card always |
| 929 |
// has a meaningful description on this screen. |
| 930 |
if ( 'charitable-pro' === $addon['slug'] && '' === trim( $description ) ) { |
| 931 |
$description = __( 'Charitable Pro is the upgraded version of your current Charitable plugin, with built-in features and compatibility for more advanced addons. Installing Pro will replace this Lite version while keeping all your existing settings and data.', 'charitable' ); |
| 932 |
} |
| 933 |
$is_recommended = ( isset( $addon['featured'] ) && is_array( $addon['featured'] ) && in_array( 'recommended', $addon['featured'] ) ) ? true : false; // phpcs:ignore |
| 934 |
|
| 935 |
// css. |
| 936 |
$css = array(); |
| 937 |
if ( $is_recommended ) { |
| 938 |
$css[] = 'has-badge'; |
| 939 |
$css[] = 'recommended'; |
| 940 |
} |
| 941 |
|
| 942 |
// Output the card. |
| 943 |
// Most addon cards strip the redundant "Charitable " prefix so the title |
| 944 |
// reads cleanly (e.g. "Fee Relief" instead of "Charitable Fee Relief"). |
| 945 |
// The Pro card is the exception — without the brand name it just reads as |
| 946 |
// "Pro", which is meaningless. Keep the full name only for that slug. |
| 947 |
$addon_name = ( 'charitable-pro' === $addon['slug'] ) |
| 948 |
? $addon['name'] |
| 949 |
: str_replace( 'Charitable ', '', $addon['name'] ); |
| 950 |
$landing_page_url = ! empty( $addon['homepage'] ) ? $addon['homepage'] : false; |
| 951 |
$landing_page_url = false === $landing_page_url ? $addon['upgrade_url'] : $landing_page_url; |
| 952 |
?> |
| 953 |
|
| 954 |
|
| 955 |
<div class="charitable-addons-list-item <?php echo esc_attr( implode( ' ', $css ) ); ?>"> |
| 956 |
<div class="charitable-addons-list-item-header"> |
| 957 |
<img src="<?php echo esc_url( $icon ); ?>" alt="<?php echo esc_html( $addon['name'] ); ?> logo" onerror="this.onerror=null;this.src='<?php echo esc_url( $placeholder_icon ); ?>';"> |
| 958 |
|
| 959 |
<div class="charitable-addons-list-item-header-meta"> |
| 960 |
<div class="charitable-addons-list-item-header-meta-title"> |
| 961 |
<a href="<?php echo esc_url( $landing_page_url ); ?>" title="Learn more" target="_blank" rel="noopener noreferrer" class="addon-link"><?php echo esc_html( $addon_name ); ?></a> |
| 962 |
<?php if ( $is_recommended ) : ?> |
| 963 |
<?php /* <span class="charitable-badge charitable-badge-sm charitable-badge-inline charitable-badge-green charitable-badge-rounded"><i class="fa fa-star" aria-hidden="true"></i>Recommended</span> */ ?> |
| 964 |
<span class="charitable-badge charitable-badge-sm charitable-badge-inline charitable-badge-orange charitable-badge-rounded">Featured</span> |
| 965 |
<?php elseif ( $addon['is_updated'] ) : ?> |
| 966 |
<span class="charitable-badge charitable-badge-sm charitable-badge-inline charitable-badge-blue charitable-badge-rounded">Updated</span> |
| 967 |
<?php endif; ?> |
| 968 |
</div> |
| 969 |
|
| 970 |
<div class="charitable-addons-list-item-header-meta-excerpt addon-description"> |
| 971 |
<?php echo wpautop( $description ); // phpcs:ignore ?> </div> |
| 972 |
</div> |
| 973 |
</div> |
| 974 |
|
| 975 |
<div class="charitable-addons-list-item-footer charitable-addons-list-item-footer-installed" data-plugin="<?php echo esc_attr( $addon['path'] ); ?>" data-type="addon"> |
| 976 |
<div> |
| 977 |
<?php if ( ! empty( $addon['license'] ) ) : ?> |
| 978 |
<?php |
| 979 |
// The license badge defaults to the highest tier the addon is |
| 980 |
// available in. Charitable Pro is available to every legitimate |
| 981 |
// license tier, so showing the "Pro" tier label is misleading — |
| 982 |
// surface "All Plans" instead so customers on Basic/Plus realise |
| 983 |
// they can install it too. |
| 984 |
$license_badge_text = ( 'charitable-pro' === $addon['slug'] ) |
| 985 |
? __( 'All Plans', 'charitable' ) |
| 986 |
: end( $addon['license'] ); |
| 987 |
?> |
| 988 |
<span class="charitable-badge charitable-badge-lg charitable-badge-inline charitable-badge-titanium charitable-badge-rounded"><?php echo esc_html( $license_badge_text ); ?></span> |
| 989 |
<?php endif; ?> |
| 990 |
</div> |
| 991 |
|
| 992 |
<div class="charitable-addons-list-item-footer-actions"> |
| 993 |
<?php echo wp_kses_post( $button ); ?> |
| 994 |
</div> |
| 995 |
</div> |
| 996 |
</div> |
| 997 |
|
| 998 |
<?php |
| 999 |
} |
| 1000 |
|
| 1001 |
/** |
| 1002 |
* Get addon button HTML. |
| 1003 |
* |
| 1004 |
* @since 1.6.7 |
| 1005 |
* |
| 1006 |
* @param array $addon Prepared addon data. |
| 1007 |
* |
| 1008 |
* @return string |
| 1009 |
*/ |
| 1010 |
private function get_addon_button_html( $addon ) { |
| 1011 |
|
| 1012 |
// Check if addon is included in pro and return appropriate text. |
| 1013 |
if ( ! empty( $addon['is_included_in_pro'] ) ) { |
| 1014 |
return '<span class="charitable-addon-included-in-pro">' . esc_html__( 'Included In Pro', 'charitable' ) . '</span>'; |
| 1015 |
} |
| 1016 |
|
| 1017 |
// Check if addon is installed and return appropriate text. |
| 1018 |
if ( 'installed_text' === $addon['status'] ) { |
| 1019 |
return '<span class="charitable-addon-installed">' . esc_html__( 'Installed', 'charitable' ) . '</span>'; |
| 1020 |
} |
| 1021 |
|
| 1022 |
if ( $addon['action'] === 'upgrade' || $addon['action'] === 'license' || ! $addon['plugin_allow'] ) { |
| 1023 |
|
| 1024 |
if ( charitable_is_pro() ) { |
| 1025 |
|
| 1026 |
// go ahead and send those who have a license key added to the checkout page, but down the road we will send them to the pricing page and then customize the prices on the pricing page to reflect the upgrade amounts per plan. |
| 1027 |
|
| 1028 |
$license_data_to_send = $this->get_license_key_for_upgrade(); |
| 1029 |
|
| 1030 |
if ( false !== $license_data_to_send && isset( $license_data_to_send['license'] ) ) { |
| 1031 |
|
| 1032 |
$upid = 0; // the upgrade plan id. |
| 1033 |
|
| 1034 |
if ( in_array( 'agency', $addon['license'] ) ) { // phpcs:ignore |
| 1035 |
// if the desired addon is in the agency category license, we need to pass along the upgrade plan id of 4. |
| 1036 |
$upid = 4; |
| 1037 |
} |
| 1038 |
|
| 1039 |
if ( in_array( 'pro', $addon['license'] ) ) { // phpcs:ignore |
| 1040 |
// if the desired addon is in the pro category license, we need to pass along the upgrade plan id of 3. |
| 1041 |
$upid = 3; |
| 1042 |
} |
| 1043 |
|
| 1044 |
if ( in_array( 'plus', $addon['license'] ) ) { // phpcs:ignore |
| 1045 |
// if the desired addon is in the plus category license, we need to pass along the upgrade plan id of 2. |
| 1046 |
$upid = 2; |
| 1047 |
} |
| 1048 |
|
| 1049 |
// we have a valid license that we could get an upgrade path, etc. from. |
| 1050 |
$upgrade_url = charitable_ga_url( |
| 1051 |
// pid = plan_id. |
| 1052 |
self::UPDATE_URL . '/?license_upgrade=true&license_key=' . $license_data_to_send['license'] . '&upid=' . $upid . '&pid=' . $license_data_to_send['plan_id'] . '&addon=' . esc_html( $addon['name'] ), |
| 1053 |
urlencode( 'Plugin Addon Page' ), // phpcs:ignore |
| 1054 |
urlencode( $addon['name'] . ' Upgrade' ) // phpcs:ignore |
| 1055 |
); |
| 1056 |
|
| 1057 |
} else { |
| 1058 |
|
| 1059 |
// they may be on 'pro' mode but they don't have a license we can send, so fall back to sending them to the pricing page. |
| 1060 |
$upgrade_url = charitable_ga_url( |
| 1061 |
'https://www.wpcharitable.com/pricing/', |
| 1062 |
urlencode( 'Plugin Addon Page' ), // phpcs:ignore |
| 1063 |
urlencode( $addon['name'] . ' Upgrade' ) // phpcs:ignore |
| 1064 |
); |
| 1065 |
|
| 1066 |
} |
| 1067 |
} else { |
| 1068 |
// they are on lite, with no license so send them to the pricing page. |
| 1069 |
$upgrade_url = charitable_ga_url( |
| 1070 |
'https://www.wpcharitable.com/pricing/', |
| 1071 |
urlencode( 'Plugin Addon Page' ), // phpcs:ignore |
| 1072 |
urlencode( $addon['name'] . ' Upgrade' ) // phpcs:ignore |
| 1073 |
); |
| 1074 |
} |
| 1075 |
|
| 1076 |
return sprintf( |
| 1077 |
'<a href="%1$s" target="_blank" rel="noopener noreferrer" class="button button-secondary">%2$s</a>', |
| 1078 |
$upgrade_url, |
| 1079 |
esc_html__( 'Upgrade Now', 'charitable' ) |
| 1080 |
); |
| 1081 |
} |
| 1082 |
|
| 1083 |
$html = ''; |
| 1084 |
|
| 1085 |
// Override to account for odd setup prior to new ownership. |
| 1086 |
$addon['path'] = str_replace( 'charitable-recurring-donations', 'charitable-recurring', $addon['path'] ); |
| 1087 |
|
| 1088 |
// Apply filter, mostly for testing and troubleshooting. |
| 1089 |
// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Legacy hook name maintained for backward compatibility with existing filters. |
| 1090 |
$addon = apply_filters( 'get_addon_button_html_addon', $addon ); |
| 1091 |
|
| 1092 |
switch ( $addon['status'] ) { |
| 1093 |
case 'active': |
| 1094 |
$html = '<button class="button button-secondary status-' . esc_attr( $addon['status'] ) . '" data-plugin="' . esc_attr( $addon['path'] ) . '" data-type="addon">'; |
| 1095 |
$html .= esc_html__( 'Deactivate', 'charitable' ); |
| 1096 |
$html .= '</button>'; |
| 1097 |
break; |
| 1098 |
|
| 1099 |
case 'installed': |
| 1100 |
$html = '<button class="button button-secondary status-' . esc_attr( $addon['status'] ) . '" data-plugin="' . esc_attr( $addon['path'] ) . '" data-type="addon">'; |
| 1101 |
$html .= esc_html__( 'Activate', 'charitable' ); |
| 1102 |
$html .= '</button>'; |
| 1103 |
break; |
| 1104 |
|
| 1105 |
case 'missing': |
| 1106 |
if ( ! $this->can_install ) { |
| 1107 |
break; |
| 1108 |
} |
| 1109 |
|
| 1110 |
// Testing backup links for download. |
| 1111 |
$download_url = ! empty( $addon['download_link'] ) ? esc_url( $addon['download_link'] ) : false; |
| 1112 |
$download_url = ( false === $download_url ) && ! empty( $addon['install'] ) ? esc_url( $addon['install'] ) : $download_url; |
| 1113 |
|
| 1114 |
// Validate the download URL - if it's invalid, disable the button |
| 1115 |
$is_valid_url = $download_url && filter_var( $download_url, FILTER_VALIDATE_URL ) && ! strpos( $download_url, 'invalid_price_id_plan__' ); |
| 1116 |
|
| 1117 |
if ( $is_valid_url ) { |
| 1118 |
$html = '<button class="button button-secondary status-' . esc_attr( $addon['status'] ) . '" data-plugin="' . esc_url( $download_url ) . '" data-type="addon">'; |
| 1119 |
$html .= esc_html__( 'Install Addon', 'charitable' ); |
| 1120 |
$html .= '</button>'; |
| 1121 |
} else { |
| 1122 |
// Invalid URL - disable button and add warning class |
| 1123 |
$html = '<button class="button button-secondary status-' . esc_attr( $addon['status'] ) . ' disabled" data-plugin="invalid" data-type="addon" disabled>'; |
| 1124 |
$html .= esc_html__( 'Install Addon', 'charitable' ); |
| 1125 |
$html .= '</button>'; |
| 1126 |
} |
| 1127 |
break; |
| 1128 |
} |
| 1129 |
|
| 1130 |
return $html; |
| 1131 |
} |
| 1132 |
|
| 1133 |
|
| 1134 |
/** |
| 1135 |
* Check if any addons have invalid download URLs and display admin notice. |
| 1136 |
* |
| 1137 |
* @since 1.8.8 |
| 1138 |
* @version 1.8.9.1 |
| 1139 |
*/ |
| 1140 |
public function check_for_invalid_urls() { |
| 1141 |
$addons = $this->get_addons(); |
| 1142 |
$has_invalid_urls = false; |
| 1143 |
|
| 1144 |
foreach ( $addons as $addon ) { |
| 1145 |
if ( 'missing' === $addon['status'] ) { |
| 1146 |
$download_url = ! empty( $addon['download_link'] ) ? $addon['download_link'] : false; |
| 1147 |
$download_url = ( false === $download_url ) && ! empty( $addon['install'] ) ? $addon['install'] : $download_url; |
| 1148 |
|
| 1149 |
if ( ! $download_url || ! filter_var( $download_url, FILTER_VALIDATE_URL ) || strpos( $download_url, 'invalid_price_id_plan__' ) !== false ) { |
| 1150 |
$has_invalid_urls = true; |
| 1151 |
break; |
| 1152 |
} |
| 1153 |
} |
| 1154 |
} |
| 1155 |
|
| 1156 |
if ( $has_invalid_urls ) { |
| 1157 |
$settings_url = admin_url( 'admin.php?page=charitable-settings' ); |
| 1158 |
$notice = sprintf( |
| 1159 |
'<div class="notice notice-warning is-dismissible"><p><strong>%s</strong> %s <a href="%s">%s</a></p></div>', |
| 1160 |
esc_html__( 'License Data Warning:', 'charitable' ), |
| 1161 |
esc_html__( 'Some addon download URLs appear to be invalid. This may indicate that your license data is not fully synchronized with the license server. You may need to reconnect your license by removing it and adding it back in the', 'charitable' ), |
| 1162 |
esc_url( $settings_url ), |
| 1163 |
esc_html__( 'Charitable Settings page', 'charitable' ) |
| 1164 |
); |
| 1165 |
|
| 1166 |
echo wp_kses_post( $notice ); |
| 1167 |
} |
| 1168 |
} |
| 1169 |
|
| 1170 |
/** |
| 1171 |
* Get addon status label. |
| 1172 |
* |
| 1173 |
* @since 1.6.7 |
| 1174 |
* |
| 1175 |
* @param string $status Addon status. |
| 1176 |
* |
| 1177 |
* @return string |
| 1178 |
*/ |
| 1179 |
private function get_addon_status_label( $status ) { |
| 1180 |
|
| 1181 |
$label = [ |
| 1182 |
'active' => esc_html__( 'Active', 'charitable' ), |
| 1183 |
'installed' => esc_html__( 'Inactive', 'charitable' ), |
| 1184 |
'missing' => esc_html__( 'Not Installed', 'charitable' ), |
| 1185 |
]; |
| 1186 |
|
| 1187 |
return isset( $label[ $status ] ) ? $label[ $status ] : ''; |
| 1188 |
} |
| 1189 |
|
| 1190 |
/** |
| 1191 |
* Check if addon is new. |
| 1192 |
* This examines 'last_updated' from the addon data and compares it to the current date. |
| 1193 |
* |
| 1194 |
* @since 1.8.8 |
| 1195 |
* |
| 1196 |
* @param array $addon Addon data. |
| 1197 |
* |
| 1198 |
* @return bool |
| 1199 |
*/ |
| 1200 |
public function is_addon_updated( $addon ) { |
| 1201 |
|
| 1202 |
if ( empty( $addon['last_updated'] ) ) { |
| 1203 |
return false; |
| 1204 |
} |
| 1205 |
|
| 1206 |
$addon_date = strtotime( $addon['last_updated'] ); |
| 1207 |
$current_date = strtotime( date( 'Y-m-d' ) ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date |
| 1208 |
|
| 1209 |
// If the last_updated is within 90 days, return true. |
| 1210 |
return $addon_date > $current_date - 90 * DAY_IN_SECONDS; |
| 1211 |
} |
| 1212 |
|
| 1213 |
/** |
| 1214 |
* Check if addon is new. |
| 1215 |
* |
| 1216 |
* @since 1.8.8 |
| 1217 |
* |
| 1218 |
* @param array $addon Addon data. |
| 1219 |
* |
| 1220 |
* @return bool |
| 1221 |
*/ |
| 1222 |
public function is_addon_new( $addon ) { |
| 1223 |
|
| 1224 |
return false; |
| 1225 |
} |
| 1226 |
|
| 1227 |
/** |
| 1228 |
* Prepare addon data. |
| 1229 |
* |
| 1230 |
* @since 1.6.6 |
| 1231 |
* |
| 1232 |
* @param array $addon Addon data. |
| 1233 |
* |
| 1234 |
* @return array Extended addon data. |
| 1235 |
*/ |
| 1236 |
protected function prepare_addon_data( $addon ) { |
| 1237 |
|
| 1238 |
if ( empty( $addon ) ) { |
| 1239 |
return []; |
| 1240 |
} |
| 1241 |
|
| 1242 |
$addon['title'] = ! empty( $addon['title'] ) ? $addon['title'] : ''; |
| 1243 |
$addon['slug'] = ! empty( $addon['slug'] ) ? $addon['slug'] : ''; |
| 1244 |
|
| 1245 |
/* translators: %s - addon name. */ |
| 1246 |
$addon['modal_name'] = sprintf( esc_html__( '%s addon', 'charitable' ), $addon['name'] ); |
| 1247 |
$addon['clear_slug'] = str_replace( 'charitable-', '', $addon['slug'] ); |
| 1248 |
$addon['utm_content'] = ucwords( str_replace( '-', ' ', $addon['clear_slug'] ) ); |
| 1249 |
$addon['license'] = empty( $addon['license'] ) ? [] : (array) $addon['license']; |
| 1250 |
$addon['license_level'] = $this->get_license_level( $addon ); |
| 1251 |
$addon['icon'] = ! empty( $addon['icon'] ) ? $addon['icon'] : ''; |
| 1252 |
$addon['path'] = sprintf( '%1$s/%1$s.php', $addon['slug'] ); |
| 1253 |
$addon['video'] = ! empty( $addon['video'] ) ? $addon['video'] : ''; |
| 1254 |
$addon['plugin_allow'] = $this->has_access( $addon ); |
| 1255 |
$addon['status'] = 'missing'; |
| 1256 |
$addon['action'] = 'upgrade'; |
| 1257 |
$addon['page_url'] = empty( $addon['url'] ) ? '' : $addon['url']; |
| 1258 |
$addon['doc_url'] = empty( $addon['doc'] ) ? '' : $addon['doc']; |
| 1259 |
$addon['url'] = ''; |
| 1260 |
|
| 1261 |
static $nonce = ''; |
| 1262 |
$nonce = empty( $nonce ) ? wp_create_nonce( 'charitable-admin' ) : $nonce; |
| 1263 |
$addon['nonce'] = $nonce; |
| 1264 |
|
| 1265 |
return $addon; |
| 1266 |
} |
| 1267 |
|
| 1268 |
|
| 1269 |
/** |
| 1270 |
* Get available addon data by slug. |
| 1271 |
* |
| 1272 |
* @since 1.6.6 |
| 1273 |
* |
| 1274 |
* @param string $slug Addon slug, can be both "charitable-drip" and "drip". |
| 1275 |
* |
| 1276 |
* @return array Single addon data. Empty array if addon is not found. |
| 1277 |
*/ |
| 1278 |
public function get_addon( $slug ) { |
| 1279 |
|
| 1280 |
$slug = 'charitable-' . str_replace( 'charitable-', '', sanitize_key( $slug ) ); |
| 1281 |
|
| 1282 |
$addon = ! empty( $this->available_addons[ $slug ] ) ? $this->available_addons[ $slug ] : []; |
| 1283 |
|
| 1284 |
// In case if addon is "not available" let's try to get and prepare addon data from all addons. |
| 1285 |
if ( empty( $addon ) ) { |
| 1286 |
$addon = ! empty( $this->addons[ $slug ] ) ? $this->prepare_addon_data( $this->addons[ $slug ] ) : []; |
| 1287 |
} |
| 1288 |
|
| 1289 |
return $addon; |
| 1290 |
} |
| 1291 |
|
| 1292 |
/** |
| 1293 |
* Get license level of the addon. |
| 1294 |
* |
| 1295 |
* @since 1.6.6 |
| 1296 |
* |
| 1297 |
* @param array|string $addon Addon data array OR addon slug. |
| 1298 |
* |
| 1299 |
* @return string License level: pro | elite. |
| 1300 |
*/ |
| 1301 |
private function get_license_level( $addon ) { |
| 1302 |
|
| 1303 |
if ( empty( $addon ) ) { |
| 1304 |
return ''; |
| 1305 |
} |
| 1306 |
|
| 1307 |
$addon = is_string( $addon ) ? $this->get_addon( $addon ) : $addon; |
| 1308 |
$addon['license'] = empty( $addon['license'] ) ? [] : (array) $addon['license']; |
| 1309 |
|
| 1310 |
// TODO: convert to a class constant when we will drop PHP 5.5. |
| 1311 |
$levels = [ 'basic', 'plus', 'pro', 'elite', 'agency', 'ultimate' ]; |
| 1312 |
$license = ''; |
| 1313 |
|
| 1314 |
foreach ( $levels as $level ) { |
| 1315 |
if ( in_array( $level, $addon['license'], true ) ) { |
| 1316 |
$license = $level; |
| 1317 |
|
| 1318 |
break; |
| 1319 |
} |
| 1320 |
} |
| 1321 |
|
| 1322 |
if ( empty( $license ) ) { |
| 1323 |
return ''; |
| 1324 |
} |
| 1325 |
|
| 1326 |
return in_array( $license, [ 'basic', 'plus', 'pro' ], true ) ? 'pro' : 'elite'; |
| 1327 |
} |
| 1328 |
|
| 1329 |
/** |
| 1330 |
* This attempts to send the BEST license key to Charitable whenever a suggested upgrade is possible. |
| 1331 |
* Note: We look for "v2" licenses in settings first, then attempt to locate a charitable basic/plus/pro/agency (new or oldschool) license. |
| 1332 |
* We don't return individual addons in this function because there's no good way to suggest an upgrade EDD license path. |
| 1333 |
* |
| 1334 |
* @since 1.7.0.4 |
| 1335 |
* |
| 1336 |
* @return array |
| 1337 |
*/ |
| 1338 |
private function get_license_key_for_upgrade() { |
| 1339 |
|
| 1340 |
$settings = get_option( 'charitable_settings' ); |
| 1341 |
if ( isset( $settings['licenses']['charitable-v2']['license'] ) && ! empty( $settings['licenses']['charitable-v2']['license'] ) ) { |
| 1342 |
|
| 1343 |
// attempt to grab the "v2" license. |
| 1344 |
$license = esc_html( $settings['licenses']['charitable-v2']['license'] ); |
| 1345 |
$plan_id = isset( $settings['licenses']['charitable-v2']['plan_id'] ) && ! empty( $settings['licenses']['charitable-v2']['plan_id'] ) ? intval( $settings['licenses']['charitable-v2']['plan_id'] ) : false; |
| 1346 |
return array( |
| 1347 |
'license' => $license, |
| 1348 |
'plan_id' => $plan_id, |
| 1349 |
); |
| 1350 |
|
| 1351 |
} elseif ( isset( $settings['licenses']['charitable'] ) && ! empty( $settings['licenses']['charitable'] ) ) { |
| 1352 |
|
| 1353 |
$license = esc_html( $settings['licenses']['charitable'] ); |
| 1354 |
return array( |
| 1355 |
'license' => $license, |
| 1356 |
'plan_id' => false, |
| 1357 |
); // no plan id when this was added into settings. |
| 1358 |
|
| 1359 |
} elseif ( isset( $settings['licenses']['charitable_pro'] ) && ! empty( $settings['licenses']['charitable_pro'] ) ) { |
| 1360 |
|
| 1361 |
// unlikely but technically possible. |
| 1362 |
$license = esc_html( $settings['licenses']['charitable_pro'] ); |
| 1363 |
return array( |
| 1364 |
'license' => $license, |
| 1365 |
'plan_id' => false, |
| 1366 |
); // no plan id when this was added into settings. |
| 1367 |
|
| 1368 |
} |
| 1369 |
|
| 1370 |
return false; |
| 1371 |
} |
| 1372 |
|
| 1373 |
/** |
| 1374 |
* Determine if user's license level has access. |
| 1375 |
* |
| 1376 |
* @since 1.6.6 |
| 1377 |
* |
| 1378 |
* @param array|string $addon Addon data array OR addon slug. |
| 1379 |
* |
| 1380 |
* @return bool |
| 1381 |
*/ |
| 1382 |
protected function has_access( $addon ) { // phpcs:ignore |
| 1383 |
|
| 1384 |
return true; |
| 1385 |
} |
| 1386 |
|
| 1387 |
|
| 1388 |
/** |
| 1389 |
* Return the latest versions of Charitable plugins. |
| 1390 |
* |
| 1391 |
* @since 1.7.0.4 |
| 1392 |
* |
| 1393 |
* @param array $licenses Array of license keys. |
| 1394 |
* |
| 1395 |
* @return array |
| 1396 |
*/ |
| 1397 |
public function get_addons_data_from_server( $licenses = false ) { |
| 1398 |
|
| 1399 |
$versions = get_transient( '_charitable_plugin_versions' ); |
| 1400 |
|
| 1401 |
if ( false === $versions ) { |
| 1402 |
|
| 1403 |
if ( false === $licenses ) { |
| 1404 |
|
| 1405 |
$licenses = array(); |
| 1406 |
|
| 1407 |
foreach ( $this->get_licenses() as $license ) { |
| 1408 |
if ( isset( $license['license'] ) ) { |
| 1409 |
$licenses[] = $license['license']; |
| 1410 |
} |
| 1411 |
} |
| 1412 |
} |
| 1413 |
|
| 1414 |
$response = wp_remote_post( |
| 1415 |
self::UPDATE_URL . '/edd-api/versions-v3/', |
| 1416 |
array( |
| 1417 |
'sslverify' => false, |
| 1418 |
'timeout' => 15, |
| 1419 |
'body' => array( |
| 1420 |
'licenses' => $licenses, |
| 1421 |
'url' => home_url(), |
| 1422 |
), |
| 1423 |
) |
| 1424 |
); |
| 1425 |
|
| 1426 |
$response_body = wp_remote_retrieve_body( $response ); |
| 1427 |
$response_code = wp_remote_retrieve_response_code( $response ); |
| 1428 |
|
| 1429 |
// Bail out early if there are any errors. |
| 1430 |
if ( (int) $response_code !== 200 || is_wp_error( $response_body ) ) { |
| 1431 |
return false; |
| 1432 |
} |
| 1433 |
|
| 1434 |
$versions = json_decode( $response_body, true ); |
| 1435 |
|
| 1436 |
set_transient( '_charitable_plugin_versions', $versions, DAY_IN_SECONDS ); |
| 1437 |
|
| 1438 |
} // end if |
| 1439 |
|
| 1440 |
return $versions; |
| 1441 |
} |
| 1442 |
|
| 1443 |
/** |
| 1444 |
* Return the latest versions of Charitable plugins. |
| 1445 |
* |
| 1446 |
* @since 1.4.0 |
| 1447 |
* @since 1.8.1.1 - Removed cache_get method and site a site option instead. |
| 1448 |
* @since 1.8.1.3 - Added request_recently_failed check and log_failed_request call. |
| 1449 |
* |
| 1450 |
* @return array |
| 1451 |
*/ |
| 1452 |
protected function get_versions() { |
| 1453 |
|
| 1454 |
$versions = get_site_option( 'wpc_plugin_versions' ); |
| 1455 |
|
| 1456 |
if ( false === $versions || ! isset( $versions['expires'] ) || time() > $versions['expires'] || ! isset( $versions['data'] ) ) { |
| 1457 |
/* |
| 1458 |
* Check and see if this request recently failed. |
| 1459 |
* Most likely because the site is down or didn't return a 200 response. |
| 1460 |
* Regardless this means we're allowed to try again for a while. |
| 1461 |
*/ |
| 1462 |
if ( $this->request_recently_failed() ) { |
| 1463 |
if ( charitable_is_debug() ) { |
| 1464 |
error_log( 'WPCharitable Debug Error: get_versions (licenses) API call aborted because it recently failed' ); // @codingStandardsIgnoreLine |
| 1465 |
} |
| 1466 |
return false; |
| 1467 |
} |
| 1468 |
|
| 1469 |
$licenses = array(); |
| 1470 |
|
| 1471 |
foreach ( $this->get_licenses() as $license ) { |
| 1472 |
if ( isset( $license['license'] ) ) { |
| 1473 |
$licenses[] = $license['license']; |
| 1474 |
} |
| 1475 |
} |
| 1476 |
|
| 1477 |
$response = wp_remote_post( |
| 1478 |
self::UPDATE_URL . '/edd-api/versions-v2/', |
| 1479 |
array( |
| 1480 |
'sslverify' => false, |
| 1481 |
'timeout' => 15, |
| 1482 |
'body' => array( |
| 1483 |
'licenses' => $licenses, |
| 1484 |
'url' => home_url(), |
| 1485 |
), |
| 1486 |
) |
| 1487 |
); |
| 1488 |
|
| 1489 |
// check the response code... if it's bad, then return what we have if anything. |
| 1490 |
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { |
| 1491 |
|
| 1492 |
// Log this failure so we don't keep trying. |
| 1493 |
$this->log_failed_request(); |
| 1494 |
|
| 1495 |
if ( charitable_is_debug() ) { |
| 1496 |
error_log( 'WPCharitable Debug Error: get_versions (licenses) API call failed' ); // @codingStandardsIgnoreLine |
| 1497 |
error_log( print_r( $response, true ) ); // @codingStandardsIgnoreLine |
| 1498 |
} |
| 1499 |
|
| 1500 |
return ( false !== $versions && isset( $versions['data'] ) ) ? $versions['data'] : array(); |
| 1501 |
} |
| 1502 |
|
| 1503 |
$versions = wp_remote_retrieve_body( $response ); |
| 1504 |
|
| 1505 |
$versions = json_decode( $versions, true ); |
| 1506 |
|
| 1507 |
update_site_option( |
| 1508 |
'wpc_plugin_versions', |
| 1509 |
array( |
| 1510 |
'expires' => strtotime( '+1 hour', time() ), |
| 1511 |
'data' => $versions, |
| 1512 |
) |
| 1513 |
); |
| 1514 |
|
| 1515 |
if ( charitable_is_debug() ) { |
| 1516 |
error_log( 'WPCharitable Debug Notice: get_versions (licenses) API call in addons directory successful and added to site option.' ); // @codingStandardsIgnoreLine |
| 1517 |
error_log( // @codingStandardsIgnoreLine |
| 1518 |
print_r( // @codingStandardsIgnoreLine |
| 1519 |
array( |
| 1520 |
'expires' => strtotime( '+1 hour', time() ), |
| 1521 |
'data' => $versions, |
| 1522 |
), |
| 1523 |
true |
| 1524 |
) |
| 1525 |
); |
| 1526 |
} |
| 1527 |
} else { |
| 1528 |
$versions = $versions['data']; |
| 1529 |
} |
| 1530 |
|
| 1531 |
return $versions; |
| 1532 |
} |
| 1533 |
|
| 1534 |
|
| 1535 |
/** |
| 1536 |
* Logs a failed HTTP request for this API URL. |
| 1537 |
* |
| 1538 |
* We set a timestamp for 1 hour from now. This prevents future API requests from being |
| 1539 |
* made to this domain for 1 hour. Once the timestamp is in the past, API requests |
| 1540 |
* will be allowed again. This way if the site is down for some reason we don't bombard |
| 1541 |
* it with failed API requests. |
| 1542 |
* |
| 1543 |
* @see EDD_SL_Plugin_Updater::request_recently_failed |
| 1544 |
* |
| 1545 |
* @since 1.8.1.3 |
| 1546 |
*/ |
| 1547 |
private function log_failed_request() { |
| 1548 |
update_option( $this->failed_request_cache_key, strtotime( '+1 hour' ) ); |
| 1549 |
} |
| 1550 |
|
| 1551 |
/** |
| 1552 |
* Determines if a request has recently failed. |
| 1553 |
* |
| 1554 |
* @since 1.8.1.3 |
| 1555 |
* |
| 1556 |
* @return bool |
| 1557 |
*/ |
| 1558 |
private function request_recently_failed() { |
| 1559 |
$failed_request_details = get_option( $this->failed_request_cache_key ); |
| 1560 |
|
| 1561 |
// Request has never failed. |
| 1562 |
if ( empty( $failed_request_details ) || ! is_numeric( $failed_request_details ) ) { |
| 1563 |
return false; |
| 1564 |
} |
| 1565 |
|
| 1566 |
/* |
| 1567 |
* Request previously failed, but the timeout has expired. |
| 1568 |
* This means we're allowed to try again. |
| 1569 |
*/ |
| 1570 |
if ( time() > $failed_request_details ) { |
| 1571 |
delete_option( $this->failed_request_cache_key ); |
| 1572 |
|
| 1573 |
return false; |
| 1574 |
} |
| 1575 |
|
| 1576 |
return true; |
| 1577 |
} |
| 1578 |
|
| 1579 |
/** |
| 1580 |
* Return the list of licenses. |
| 1581 |
* |
| 1582 |
* Note: The licenses are not necessarily valid. If a user enters an invalid |
| 1583 |
* license, the license will be stored but it will be flagged as invalid. |
| 1584 |
* |
| 1585 |
* @since 1.0.0 |
| 1586 |
* |
| 1587 |
* @return array[] |
| 1588 |
*/ |
| 1589 |
public function get_licenses() { |
| 1590 |
if ( ! isset( $this->licenses ) ) { |
| 1591 |
$this->licenses = charitable_get_option( 'licenses', array() ); |
| 1592 |
} |
| 1593 |
|
| 1594 |
return $this->licenses; |
| 1595 |
} |
| 1596 |
} |
| 1597 |
|
| 1598 |
endif; |
| 1599 |
|