PluginProbe
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) / 1.6.60
Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) v1.6.60
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 / licensing / class-charitable-licenses.php

class-charitable-licenses.php in Charitable – Donation & Fundraising Platform (Donation Forms, Recurring Donations & Fundraising Campaigns) 1.6.60, at includes/licensing/class-charitable-licenses.php

802 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class to assist with the setup of extension licenses.
4 *
5 * @package Charitable/Classes/Charitable_Licenses
6 * @version 1.4.20
7 * @author Eric Daams
8 * @copyright Copyright (c) 2022, Studio 164a
9 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
10 */
11
12 // Exit if accessed directly.
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 if ( ! class_exists( 'Charitable_Licenses' ) ) :
18
19 /**
20 * Charitable_Licenses
21 *
22 * @since 1.0.0
23 */
24 class Charitable_Licenses {
25
26 /* @var string */
27 const UPDATE_URL = 'https://www.wpcharitable.com';
28
29 /**
30 * The single instance of this class.
31 *
32 * @var Charitable_Licenses|null
33 */
34 private static $instance = null;
35
36 /**
37 * All the registered products requiring licensing.
38 *
39 * @var array
40 */
41 private $products;
42
43 /**
44 * All the stored licenses.
45 *
46 * @var array
47 */
48 private $licenses;
49
50 /**
51 * Cached update data.
52 *
53 * @var array
54 */
55 private $update_data;
56
57 /**
58 * Returns and/or create the single instance of this class.
59 *
60 * @since 1.2.0
61 *
62 * @return Charitable_Licenses
63 */
64 public static function get_instance() {
65 if ( is_null( self::$instance ) ) {
66 self::$instance = new self();
67 }
68
69 return self::$instance;
70 }
71
72 /**
73 * Create class object.
74 *
75 * @since 1.0.0
76 */
77 private function __construct() {
78 $this->products = array();
79 $this->update_data = array();
80
81 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
82 add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
83 add_action( 'charitable_deactivate_license', array( $this, 'deactivate_license' ) );
84 add_filter( 'upgrader_pre_download', array( $this, 'set_upgrader_error_message' ), 10, 3 );
85 add_filter( 'upgrader_package_options', array( $this, 'set_upgrader_package_options' ) );
86 }
87
88 /**
89 * Checks for any Charitable extensions with updates.
90 *
91 * @since 1.4.0
92 *
93 * @param array $_transient_data The plugin updates data.
94 * @return array
95 */
96 public function check_for_updates( $_transient_data ) {
97 global $pagenow;
98
99 if ( ! is_object( $_transient_data ) ) {
100 $_transient_data = new stdClass;
101 }
102
103 if ( 'plugins.php' == $pagenow && is_multisite() ) {
104 return $_transient_data;
105 }
106
107 /* Loop over our licensed products and check whether any are missing transient data. */
108 $missing_data = array();
109
110 foreach ( $this->get_products() as $product ) {
111 if ( $this->is_missing_version_info( $product, $_transient_data ) ) {
112 $missing_data[] = $product;
113 }
114 }
115
116 /* If we are missing data for any of our products, check whether any have an update. */
117 if ( ! empty( $missing_data ) ) {
118
119 $versions = $this->get_versions();
120
121 unset( $versions['request_speed'] );
122
123 if ( ! empty( $versions ) ) {
124
125 $versions_name_lookup = wp_list_pluck( $versions, 'name' );
126
127 foreach ( $missing_data as $product ) {
128
129 if ( ! in_array( $product['name'], $versions_name_lookup ) ) {
130 continue;
131 }
132
133 $plugin_file = plugin_basename( $product['file'] );
134 $product_key = array_search( $product['name'], wp_list_pluck( $this->get_products(), 'name' ) );
135 $version_info = $versions[ array_search( $product['name'], $versions_name_lookup ) ];
136 $version_info['license'] = $this->get_license_details( $product_key );
137
138 if ( version_compare( $product['version'], $version_info['new_version'], '<' ) ) {
139
140 if ( isset( $version_info['sections'] ) ) {
141 $version_info['sections'] = maybe_unserialize( $version_info['sections'] );
142 }
143
144 $can_update = $this->able_to_update( $version_info );
145
146 if ( is_array( $can_update ) ) {
147 $version_info['package'] = $can_update['reason_code'];
148 $version_info['download_link'] = $can_update['reason_code'];
149 $version_info['package_download_restriction'] = $can_update['description'];
150 }
151
152 $_transient_data->response[ $plugin_file ] = (object) $version_info;
153 }
154
155 $_transient_data->last_checked = time();
156 $_transient_data->checked[ $plugin_file ] = $product['version'];
157
158 }//end foreach
159 }//end if
160 }//end if
161
162 return $_transient_data;
163 }
164
165 /**
166 * Updates information on the "View version x.x details" page with custom data.
167 *
168 * @uses api_request()
169 *
170 * @since 1.4.20
171 *
172 * @param mixed $_data Default set of data.
173 * @param string $_action The current action.
174 * @param object $_args Request args.
175 * @return object $_data
176 */
177 public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
178 if ( 'plugin_information' != $_action ) {
179 return $_data;
180 }
181
182 if ( ! isset( $_args->slug ) ) {
183 return $_data;
184 }
185
186 $plugin_key = str_replace( '-', '_', $_args->slug );
187
188 if ( ! array_key_exists( $plugin_key, $this->products ) ) {
189 return $_data;
190 }
191
192 $version_info = $this->get_version_info( plugin_basename( $this->products[ $plugin_key ]['file'] ) );
193
194 if ( $version_info ) {
195 $_data = $version_info;
196 }
197
198 return $_data;
199 }
200
201 /**
202 * Return whether a particular plugin is missing version info.
203 *
204 * @since 1.4.20
205 *
206 * @param array $product Product details array.
207 * @param false|object $update_cache Optional argument to pass update cache.
208 * @return boolean
209 */
210 public function is_missing_version_info( $product, $update_cache = false ) {
211 return ! $this->get_version_info( plugin_basename( $product['file'] ), $update_cache );
212 }
213
214 /**
215 * Return the version update info for a particular plugin.
216 *
217 * @since 1.4.20
218 *
219 * @param string $slug The plugin slug.
220 * @param false|object $update_cache Optional argument to pass update cache.
221 * @return array|false Array if an update is available. False otherwise.
222 */
223 public function get_version_info( $slug, $update_cache = false ) {
224 if ( ! $update_cache ) {
225 $update_cache = get_site_transient( 'update_plugins' );
226 }
227
228 if ( ! is_object( $update_cache ) || empty( $update_cache->response ) || ! array_key_exists( $slug, $update_cache->response ) ) {
229 return false;
230 }
231
232 return $update_cache->response[ $slug ];
233 }
234
235 /**
236 * Display an error message when users attempt to update a plugin
237 * without a license or with an expired license.
238 *
239 * @since 1.4.20
240 *
241 * @param false|WP_Error $reply The messaage to return. Set to false by default.
242 * @param string $package Package URL. For Charitable extensions without a
243 * license or that have expired, we set this to a
244 * key. Kind of a hack to pass a message to this hook.
245 * @param Plugin_Upgrader $upgrader The Plugin_Upgrader object.
246 * @return false|WP_Error
247 */
248 public function set_upgrader_error_message( $reply, $package, $upgrader ) {
249 $ajax_skin = 'WP_Ajax_Upgrader_Skin' == get_class( $upgrader->skin );
250
251 if ( 'missing_license' == $package ) {
252 if ( $ajax_skin ) {
253 $message = sprintf( __( 'You have not activated your license key. Activate your license to update: %s', 'charitable' ),
254 admin_url( 'admin.php?page=charitable-settings&tab=licenses' )
255 );
256 } else {
257 $message = sprintf( __( 'You have not activated your license key. <a href="%s" target="_top">Activate your license to update.</a>', 'charitable' ),
258 admin_url( 'admin.php?page=charitable-settings&tab=licenses' )
259 );
260 }
261
262 return new WP_Error( 'missing_license_key', $message );
263 }
264
265 if ( false !== strpos( $package, 'expired_license:' ) ) {
266
267 $renewal_link = str_replace( 'expired_license:', '', $package );
268
269 if ( $ajax_skin ) {
270 $message = sprintf( __( 'Your license has expired. Renew your license key: %s', 'charitable' ),
271 $renewal_link
272 );
273 } else {
274 $message = sprintf( __( 'Your license has expired. <a href="%s" target="_blank">Renew your license key.</a>', 'charitable' ),
275 esc_url( $renewal_link )
276 );
277 }
278
279 return new WP_Error( 'expired_license_key', $message );
280 }
281
282 if ( false !== strpos( $package, 'missing_requirements:' ) ) {
283 $message = str_replace( 'missing_requirements:', '', htmlspecialchars_decode( $package ) );
284
285 return new WP_Error( 'missing_requirements_key', $message );
286 }
287
288 return $reply;
289 }
290
291 /**
292 * Checks whether the given plugin can be updated.
293 *
294 * @since 1.6.14
295 *
296 * @param array $version_info Version information.
297 * @return true|array If it can be updated, returns true. Otherwise
298 * returns an array with a reason_code and description.
299 */
300 protected function able_to_update( $version_info ) {
301 $changelog_link = self_admin_url( 'index.php?edd_sl_action=view_plugin_changelog&plugin=' . $version_info['name'] . '&slug=' . $version_info['slug'] . '&TB_iframe=true&width=772&height=911' );
302
303 switch ( $version_info['package'] ) {
304
305 case 'missing_license':
306 return array(
307 'reason_code' => 'missing_license',
308 'description' => sprintf(
309 __( '<p>There is a new version of %1$s available but you have not activated your license. <a target="_top" href="%2$s">Activate your license</a> or <a target="_blank" class="thickbox" href="%3$s">view version %4$s details</a>.</p>', 'charitable' ),
310 esc_html( $version_info['name'] ),
311 admin_url( 'admin.php?page=charitable-settings&tab=licenses' ),
312 esc_url( $changelog_link ),
313 esc_html( $version_info['new_version'] )
314 ),
315 );
316
317 case 'expired_license':
318 $base_renewal_url = isset( $version_info['renewal_link'] ) ? $version_info['renewal_link'] : 'https://www.wpcharitable.com/account';
319
320 return array(
321 'reason_code' => 'expired_license',
322 'description' => sprintf(
323 __( '<p>There is a new version of %1$s available but your license has expired. <a target="_blank" href="%2$s">Renew your license</a> or <a target="_blank" class="thickbox" href="%3$s">view version %4$s details</a>.</p>', 'charitable' ),
324 esc_html( $version_info['name'] ),
325 esc_url( add_query_arg( array(
326 'utm_source' => 'plugin-upgrades',
327 'utm_medium' => 'wordpress-dashboard',
328 'utm_campaign' => 'expired-license',
329 ), $base_renewal_url ) ),
330 esc_url( $changelog_link ),
331 esc_html( $version_info['new_version'] )
332 ),
333 );
334
335 default:
336 if ( ! isset( $version_info['requirements'] ) ) {
337 return true;
338 }
339
340 $messages = array();
341
342 foreach ( $version_info['requirements'] as $type => $details ) {
343
344 switch ( $type ) {
345 case 'php':
346 if ( version_compare( phpversion(), $details, '<' ) ) {
347 $messages[] = esc_html(
348 sprintf(
349 __( '<li>Requires PHP version %s or greater.</li>' ),
350 $details
351 )
352 );
353 }
354 break;
355
356 case 'charitable':
357 if ( version_compare( charitable()->get_version(), $details, '<' ) ) {
358 $messages[] = esc_html(
359 sprintf(
360 __( 'Requires Charitable version %s or greater.' ),
361 $details
362 )
363 );
364 }
365 break;
366 }
367
368 if ( empty( $messages ) ) {
369 return true;
370 }
371
372 return array(
373 'reason_code' => 'missing_requirements',
374 'description' => sprintf(
375 __( '<p>There is a new version of %1$s available but you are missing the following minimum requirements:</p>%2$s', 'charitable' ),
376 esc_html( $version_info['name'] ),
377 '<ul>' . implode( '<br/>', $messages ) . '</ul>'
378 ),
379 );
380 }
381
382 return true;
383 }
384 }
385
386 /**
387 * Set upgrader package options.
388 *
389 * This is used for expired licenses and in certain cases for products that do
390 * not have a license activated.
391 *
392 * @since 1.4.20
393 *
394 * @param array $options Upgrader package options.
395 * @return array
396 */
397 public function set_upgrader_package_options( $options ) {
398 if ( 'expired_license' != $options['package'] && empty( $options['package'] ) ) {
399 return $options;
400 }
401
402 if ( ! array_key_exists( 'hook_extra', $options ) || ! array_key_exists( 'plugin', $options['hook_extra'] ) ) {
403 return $options;
404 }
405
406 list( $plugin_key, ) = explode( '/', str_replace( '-', '_', $options['hook_extra']['plugin'] ) );
407
408 if ( ! array_key_exists( $plugin_key, $this->products ) ) {
409 return $options;
410 }
411
412 /* Set up the renewal link for expired licenses. */
413 if ( 'expired_license' == $options['package'] ) {
414 $options['package'] = $this->get_expired_license_package( $options['hook_extra']['plugin'] );
415
416 return $options;
417 }
418
419 /* Set up the renewal link for plugins where minimum requirements haven't been met. */
420 if ( 'missing_requirements' == $options['package'] ) {
421 $options['package'] = $this->get_missing_requirements_package( $options['hook_extra']['plugin'] );
422
423 return $options;
424 }
425
426 $license_details = $this->get_license_details( $plugin_key );
427
428 if ( ! is_array( $license_details ) || ! array_key_exists( 'license', $license_details ) || empty( $license_details['license'] ) ) {
429 $options['package'] = 'missing_license';
430 }
431
432 return $options;
433 }
434
435 /**
436 * Return the package string for an expired license.
437 *
438 * @since 1.4.20
439 *
440 * @param string $plugin Plugin basename.
441 * @return string
442 */
443 public function get_expired_license_package( $plugin ) {
444 $version_info = $this->get_version_info( $plugin );
445 $base_renewal_url = isset( $version_info->renewal_link ) ? $version_info->renewal_link : 'https://www.wpcharitable.com/account';
446
447 return sprintf( 'expired_license:%s', add_query_arg( array(
448 'utm_source' => 'plugin-upgrades',
449 'utm_medium' => 'wordpress-dashboard',
450 'utm_campaign' => 'expired-license',
451 ), $base_renewal_url ) );
452 }
453
454 /**
455 * Return the package string for a plugin missing requirements.
456 *
457 * @since 1.6.14
458 *
459 * @param string $plugin Plugin basename.
460 * @return string
461 */
462 public function get_missing_requirements_package( $plugin ) {
463 $version_info = $this->get_version_info( $plugin );
464
465 return sprintf(
466 'missing_requirements:%s',
467 $version_info->package_download_restriction
468 );
469 }
470
471 /**
472 * Register a product that requires licensing.
473 *
474 * @since 1.0.0
475 *
476 * @param string $item_name The title of the product.
477 * @param string $author The author of the product.
478 * @param string $version The current product version we have installed.
479 * @param string $file The path to the plugin file.
480 * @param string $url The base URL where the plugin is licensed. Defaults to Charitable_Licenses::UPDATE_URL.
481 * @return void
482 */
483 public function register_licensed_product( $item_name, $author, $version, $file, $url = false ) {
484 if ( ! $url ) {
485 $url = Charitable_Licenses::UPDATE_URL;
486 }
487
488 $product_key = $this->get_item_key( $item_name );
489
490 $this->products[ $product_key ] = array(
491 'name' => $item_name,
492 'author' => $author,
493 'version' => $version,
494 'url' => $url,
495 'file' => $file,
496 );
497
498 $licenses = $this->get_licenses();
499 $license = isset( $licenses[ $product_key ]['license'] ) ? trim( $licenses[ $product_key ]['license'] ) : '';
500
501 new Charitable_Plugin_Updater( $url, $file, array(
502 'version' => $version,
503 'license' => $license,
504 'item_name' => $item_name,
505 'author' => $author,
506 ) );
507 }
508
509 /**
510 * Return the list of products requiring licensing.
511 *
512 * @since 1.0.0
513 *
514 * @return array[]
515 */
516 public function get_products() {
517 return $this->products;
518 }
519
520 /**
521 * Return a specific product's details.
522 *
523 * @since 1.0.0
524 *
525 * @param string $item The item for which we are getting product details.
526 * @return string[]
527 */
528 public function get_product_license_details( $item ) {
529 return isset( $this->products[ $item ] ) ? $this->products[ $item ] : false;
530 }
531
532 /**
533 * Returns whether the given product has a valid license.
534 *
535 * @since 1.0.0
536 *
537 * @param string $item The item to check.
538 * @return boolean
539 */
540 public function has_valid_license( $item ) {
541 $license = $this->get_license_details( $item );
542
543 if ( ! $license || ! isset( $license['valid'] ) ) {
544 return false;
545 }
546
547 return $license['valid'];
548 }
549
550 /**
551 * Returns the license details for the given product.
552 *
553 * @since 1.0.0
554 *
555 * @param string $item The item to get the license for.
556 * @return mixed[]
557 */
558 public function get_license( $item ) {
559 $license = $this->get_license_details( $item );
560
561 if ( ! $license || ! is_array( $license ) ) {
562 return false;
563 }
564
565 return $license['license'];
566 }
567
568 /**
569 * Returns the active license details for the given product.
570 *
571 * @since 1.0.0
572 *
573 * @param string $item The item to get active licensing details for.
574 * @return mixed[]
575 */
576 public function get_license_details( $item ) {
577 $licenses = $this->get_licenses();
578
579 if ( ! isset( $licenses[ $item ] ) ) {
580 return false;
581 }
582
583 return $licenses[ $item ];
584 }
585
586 /**
587 * Return the list of licenses.
588 *
589 * Note: The licenses are not necessarily valid. If a user enters an invalid
590 * license, the license will be stored but it will be flagged as invalid.
591 *
592 * @since 1.0.0
593 *
594 * @return array[]
595 */
596 public function get_licenses() {
597 if ( ! isset( $this->licenses ) ) {
598 $this->licenses = charitable_get_option( 'licenses', array() );
599 }
600
601 return $this->licenses;
602 }
603
604 /**
605 * Verify a license.
606 *
607 * @since 1.0.0
608 *
609 * @param string $item The item to verify.
610 * @param string $license The license key for the item.
611 * @param boolean $force Whether to force the verification check.
612 * @return mixed[]
613 */
614 public function verify_license( $item, $license, $force = false ) {
615 $license = trim( $license );
616
617 if ( $license === $this->get_license( $item ) && ! $force ) {
618 return $this->get_license_details( $item );
619 }
620
621 $product_details = $this->get_product_license_details( $item );
622
623 /* This product was not correctly registered. */
624 if ( ! $product_details ) {
625 return;
626 }
627
628 /* Data to send in our API request */
629 $api_params = array(
630 'edd_action' => 'activate_license',
631 'license' => $license,
632 'item_name' => urlencode( $product_details['name'] ),
633 'url' => home_url(),
634 );
635
636 /* Call the custom API */
637 $response = wp_remote_post(
638 $product_details['url'],
639 array(
640 'timeout' => 15,
641 'sslverify' => false,
642 'body' => $api_params,
643 )
644 );
645
646 /* Make sure the response came back okay */
647 if ( is_wp_error( $response ) ) {
648 return;
649 }
650
651 $this->flush_update_cache();
652
653 $license_data = json_decode( wp_remote_retrieve_body( $response ) );
654
655 return array(
656 'license' => $license,
657 'expiration_date' => $license_data->expires,
658 'valid' => ( 'valid' === $license_data->license ),
659 );
660 }
661
662 /**
663 * Return the URL to deactivate a specific license.
664 *
665 * @since 1.0.0
666 *
667 * @param string $item The item to deactivate.
668 * @return string
669 */
670 public function get_license_deactivation_url( $item ) {
671 return esc_url( add_query_arg( array(
672 'charitable_action' => 'deactivate_license',
673 'product_key' => $item,
674 '_nonce' => wp_create_nonce( 'license' ),
675 ), admin_url( 'admin.php?page=charitable-settings&tab=licenses' ) ) );
676 }
677
678 /**
679 * Deactivate a license.
680 *
681 * @since 1.0.0
682 *
683 * @return void
684 */
685 public function deactivate_license() {
686 if ( ! wp_verify_nonce( $_REQUEST['_nonce'], 'license' ) ) {
687 wp_die( esc_attr__( 'Cheatin\' eh?!', 'charitable' ) );
688 }
689
690 $product_key = isset( $_REQUEST['product_key'] ) ? $_REQUEST['product_key'] : false;
691
692 /* Product key must be set */
693 if ( false === $product_key ) {
694 wp_die( esc_attr__( 'Missing product key', 'charitable' ) );
695 }
696
697 $product = $this->get_product_license_details( $product_key );
698
699 /* Make sure we have a valid product with a valid license. */
700 if ( ! $product || ! $this->has_valid_license( $product_key ) ) {
701 wp_die( esc_attr__( 'This product is not valid or does not have a valid license key.', 'charitable' ) );
702 }
703
704 $license = $this->get_license( $product_key );
705
706 /* Data to send to wpcharitable.com to deactivate the license. */
707 $api_params = array(
708 'edd_action' => 'deactivate_license',
709 'license' => $license,
710 'item_name' => urlencode( $product['name'] ),
711 'url' => home_url(),
712 );
713
714 /* Call the custom API. */
715 $response = wp_remote_post( $product['url'], array( 'timeout' => 15, 'sslverify' => false, 'body' => $api_params ) );
716
717 /* Make sure the response came back okay */
718 if ( is_wp_error( $response ) ) {
719 return;
720 }
721
722 $this->flush_update_cache();
723
724 /* Decode the license data */
725 $license_data = json_decode( wp_remote_retrieve_body( $response ) );
726
727 $settings = get_option( 'charitable_settings' );
728
729 unset( $settings['licenses'][ $product_key ] );
730
731 update_option( 'charitable_settings', $settings );
732 }
733
734 /**
735 * Flush the version update cache.
736 *
737 * @since 1.4.20
738 *
739 * @return void
740 */
741 protected function flush_update_cache() {
742 wp_cache_delete( 'plugin_versions', 'charitable' );
743 set_site_transient( 'update_plugins', null );
744 }
745
746 /**
747 * Return a key for the item, based on the item name.
748 *
749 * @since 1.0.0
750 *
751 * @param string $item_name Name of the item.
752 * @return string
753 */
754 protected function get_item_key( $item_name ) {
755 return strtolower( str_replace( ' ', '_', $item_name ) );
756 }
757
758 /**
759 * Return the latest versions of Charitable plugins.
760 *
761 * @since 1.4.0
762 *
763 * @return array
764 */
765 protected function get_versions() {
766 $versions = wp_cache_get( 'plugin_versions', 'charitable' );
767
768 if ( false === $versions ) {
769
770 $licenses = array();
771
772 foreach ( $this->get_licenses() as $license ) {
773 if ( isset( $license['license'] ) ) {
774 $licenses[] = $license['license'];
775 }
776 }
777
778 $response = wp_remote_post(
779 Charitable_Licenses::UPDATE_URL . '/edd-api/versions-v2/',
780 array(
781 'sslverify' => false,
782 'timeout' => 15,
783 'body' => array(
784 'licenses' => $licenses,
785 'url' => home_url(),
786 ),
787 )
788 );
789
790 $versions = wp_remote_retrieve_body( $response );
791
792 $versions = json_decode( $versions, true );
793
794 wp_cache_set( 'plugin_versions', $versions, 'charitable' );
795 }//end if
796
797 return $versions;
798 }
799 }
800
801 endif;
802