GravityView  2.17
The best, easiest way to display Gravity Forms entries on your website.
LicenseManager.php
Go to the documentation of this file.
1 <?php
2 /**
3  * @license GPL-2.0-or-later
4  *
5  * Modified by gravityview on 13-January-2023 using Strauss.
6  * @see https://github.com/BrianHenryIE/strauss
7  */
8 
10 
11 use Exception;
18 use GFForms;
19 use GFFormsModel;
20 
22  const EDD_LICENSES_API_ENDPOINT = 'https://www.gravitykit.com';
23 
25 
26  const EDD_ACTION_CHECK_LICENSE = 'check_license';
27 
28  const EDD_ACTION_ACTIVATE_LICENSE = 'activate_license';
29 
30  const EDD_ACTION_DEACTIVATE_LICENSE = 'deactivate_license';
31 
32  const HARDCODED_LICENSE_CONSTANTS = [ 'GRAVITYVIEW_LICENSE_KEY', 'GRAVITYKIT_LICENSES' ];
33 
34  /**
35  * @since 1.0.0
36  *
37  * @var LicenseManager Class instance.
38  */
39  private static $_instance;
40 
41  /**
42  * @since 1.0.0
43  *
44  * @var array Cached licenses data object.
45  */
47 
48  /**
49  * Returns class instance.
50  *
51  * @since 1.0.0
52  *
53  * @return LicenseManager
54  */
55  public static function get_instance() {
56  if ( is_null( self::$_instance ) ) {
57  self::$_instance = new self();
58  }
59 
60  return self::$_instance;
61  }
62 
63  /**
64  * Initializes the class.
65  *
66  * @since 1.0.0
67  *
68  * @return void
69  */
70  public function init() {
71  static $initialized;
72 
73  if ( $initialized ) {
74  return;
75  }
76 
77  if ( ! wp_doing_ajax() ) {
78  $this->migrate_legacy_licenses();
79 
81 
82  $this->recheck_all_licenses();
83  }
84 
85  add_filter( 'gk/foundation/ajax/' . Framework::AJAX_ROUTER . '/routes', [ $this, 'configure_ajax_routes' ] );
86 
87  add_action( 'after_plugin_row', [ $this, 'display_license_info_on_plugins_page' ], 10, 2 );
88 
90 
91  $initialized = true;
92  }
93 
94  /**
95  * Configures AJAX routes handled by this class.
96  *
97  * @since 1.0.0
98  *
99  * @see Core::process_ajax_request()
100  *
101  * @param array $routes AJAX route to class method map.
102  *
103  * @return array
104  */
105  public function configure_ajax_routes( array $routes ) {
106  return array_merge( $routes, [
107  'get_licenses' => [ $this, 'ajax_get_licenses_data' ],
108  'activate_license' => [ $this, 'ajax_activate_license' ],
109  'deactivate_license' => [ $this, 'ajax_deactivate_license' ],
110  ] );
111  }
112 
113  /**
114  * AJAX request wrapper for the get_licenses_data() method.
115  *
116  * @since 1.0.0
117  *
118  * @param array $payload
119  *
120  * @throws Exception
121  *
122  * @return array
123  */
124  public function ajax_get_licenses_data( array $payload ) {
125  if ( ! Framework::get_instance()->current_user_can( 'view_licenses' ) ) {
126  throw new Exception( esc_html__( 'You do not have a permission to perform this action.', 'gk-gravityview' ) );
127  }
128 
129  $payload = wp_parse_args( $payload, [
130  'skip_cache' => false,
131  ] );
132 
133  $this->migrate_legacy_licenses( $payload['skip_cache'] );
134 
136 
137  $this->recheck_all_licenses( $payload['skip_cache'] );
138 
139  $licenses_data = [];
140 
141  foreach ( $this->get_licenses_data() as $license ) {
142  $license = $this->modify_license_data_for_frontend_output( $license );
143  $licenses_data[ $license['key'] ] = $license;
144  }
145 
146  return $licenses_data;
147  }
148 
149  /**
150  * Retrieves license data from the database.
151  *
152  * @since 1.0.0
153  *
154  * @return array
155  */
156  public function get_licenses_data() {
157  if ( ! empty( $this->_licenses_data ) ) {
158  return $this->_licenses_data;
159  }
160 
161  $licenses_data = get_site_option( Framework::ID );
162 
163  if ( ! empty( $licenses_data ) ) {
164  $licenses_data = json_decode( Encryption::get_instance()->decrypt( $licenses_data ) ?: '', true );
165  }
166 
167  $this->_licenses_data = $licenses_data ?: [];
168 
169  return $this->_licenses_data;
170  }
171 
172  /**
173  * Saves license data in the database.
174  *
175  * @since 1.0.0
176  *
177  * @param array $licenses_data
178  *
179  * @return bool
180  */
181  public function save_licenses_data( array $licenses_data ) {
182  $expiry_dates = array_column( $licenses_data, 'expiry' );
183 
184  array_multisort( $licenses_data, SORT_ASC, $expiry_dates );
185 
186  $this->_licenses_data = $licenses_data;
187 
188  try {
189  $licenses_data = Encryption::get_instance()->encrypt( json_encode( $licenses_data ) );
190  } catch ( Exception $e ) {
191  LoggerFramework::get_instance()->error( 'Failed to encrypt licenses data: ' . $e->getMessage() );
192 
193  return false;
194  }
195 
196  return update_site_option( Framework::ID, $licenses_data );
197  }
198 
199  /**
200  * Returns an object keyed by product ID and associated licenses.
201  *
202  * @since 1.0.0
203  *
204  * @param string $key_by (optional) Key (product ID or text domain) to use for the returned array.
205  * Choices: 'id' or 'text_domain'. Default: 'id'.
206  *
207  * @return array
208  */
209  public function get_product_license_map( $key_by = 'id' ) {
210  $licenses_data = $this->get_licenses_data();
211 
212  $product_license_map = [];
213 
214  foreach ( $licenses_data as $license_key => $license_data ) {
215  if ( empty( $license_data['products'] ) ) {
216  continue;
217  }
218 
219  foreach ( $license_data['products'] as $product_id => $product_data ) {
220  switch ( $key_by ) {
221  case 'id':
222  $key = $product_id;
223  break;
224  default:
225  $key = $product_data['text_domain'];
226  break;
227  }
228 
229 
230  if ( empty( $product_license_map[ $key ] ) ) {
231  $product_license_map[ $key ] = [];
232  }
233 
234  $product_license_map[ $key ][] = $license_key;
235  }
236  }
237 
238  return $product_license_map;
239  }
240 
241  /**
242  * Returns licenses for a product.
243  *
244  * @since 1.0.0
245  *
246  * @param int|string $id Product ID or text domain.
247  *
248  * @return array
249  */
250  public function get_product_licenses( $id ) {
251  $key_by = ! ctype_alpha( $id ) ? 'id' : 'text_domain';
252 
253  $product_license_map = $this->get_product_license_map( $key_by );
254 
255  return ! empty( $product_license_map[ $id ] ) ? $product_license_map[ $id ] : [];
256  }
257 
258  /**
259  * Returns license status message based on the EDD status code.
260  *
261  * @since 1.0.0
262  *
263  * @param string $status EDD status code.
264  *
265  * @return mixed
266  */
267  public function get_license_key_status_message( $status ) {
268  $statuses = [
269  'site_inactive' => esc_html__( 'The license key is valid, but it has not been activated for this site.', 'gk-gravityview' ),
270  'inactive' => esc_html__( 'The license key is valid, but it has not been activated for this site.', 'gk-gravityview' ),
271  'no_activations_left' => esc_html__( 'This license has reached its activation limit.', 'gk-gravityview' ),
272  'deactivated' => esc_html__( 'This license has been deactivated.', 'gk-gravityview' ),
273  'valid' => esc_html__( 'This license key is valid and active.', 'gk-gravityview' ),
274  'invalid' => esc_html__( 'This license key is invalid.', 'gk-gravityview' ),
275  'missing' => esc_html__( 'This license key is invalid.', 'gk-gravityview' ),
276  'revoked' => esc_html__( 'This license key has been revoked.', 'gk-gravityview' ),
277  'expired' => esc_html__( 'This license key has expired.', 'gk-gravityview' ),
278  ];
279 
280  if ( empty( $statuses[ $status ] ) ) {
281  LoggerFramework::get_instance()->warning( 'Unknown license status: ' . $status );
282 
283  return esc_html__( 'License status could not be determined.', 'gk-gravityview' );
284  }
285 
286  return $statuses[ $status ];
287  }
288 
289  /**
290  * Performs remote call to the EDD API.
291  *
292  * @sice 1.0
293  *
294  * @param string|array $license
295  * @param string $edd_action
296  *
297  * @throws Exception
298  *
299  * @return array Response body.
300  */
301  public function perform_remote_license_call( $license, $edd_action ) {
302  $multiple_licenses = is_array( $license );
303 
304  $payload = [
305  'edd_action' => $edd_action,
306  'url' => is_multisite() ? network_home_url() : home_url(),
307  'api_version' => self::EDD_LICENSES_API_VERSION,
308  'license' => $license
309  ];
310 
311  if ( 'check_license' === $edd_action ) {
312  $payload['site_data'] = $this->get_site_data();
313  }
314 
315  try {
316  $response = Helpers::query_api(
317  self::EDD_LICENSES_API_ENDPOINT,
318  $payload
319  );
320  } catch ( Exception $e ) {
321  throw new Exception( $e->getMessage() );
322  }
323 
324  // Response can be a multidimensional array when checking multiple licenses.
325  $response = $multiple_licenses ? $response : [ $response ];
326 
327  // When checking multiple licenses (i.e., an array of keys) but there is only 1 key in the array, the response is an associative array that needs to be converted to a multidimensional array keyed by the license key.
328  if ( $multiple_licenses && 1 === count( $license ) ) {
329  $response = [ $license[0] => $response ];
330  }
331 
332  $normalized_response_data = [];
333 
334  $license_keys = $multiple_licenses ? $license : [ $license ];
335 
336  foreach ( (array) $response as $key => $data ) {
337  if ( ! isset( $data['success'] ) || ! isset( $data['license'] ) || ! isset( $data['checksum'] ) ) {
338  throw new Exception( esc_html__( 'License data received from the API is incomplete.', 'gk-gravityview' ) );
339  }
340 
341  $license_key = $multiple_licenses ? $key : $license;
342 
343  if ( ! in_array( $license_key, $license_keys, true ) ) {
344  LoggerFramework::get_instance()->warning( "EDD API returned unknown license key in response: {$license_key}" );
345 
346  continue;
347  }
348 
349  if ( ! $data['success'] && empty( $data['expires'] ) ) {
350  $expiry = null;
351  } else {
352  $expiry = ! empty( $data['expires'] ) ? strtotime( $data['expires'], current_time( 'timestamp' ) ) : null;
353  $expiry = $expiry ?: $data['expires'];
354  }
355 
356  $normalized_license_data = [
357  'name' => ! empty( $data['customer_name'] ) ? $data['customer_name'] : null,
358  'email' => ! empty( $data['customer_email'] ) ? $data['customer_email'] : null,
359  'license_name' => ! empty( $data['license_name'] ) ? $data['license_name'] : null,
360  'expiry' => $expiry,
361  'key' => $license_key,
362  'products' => [],
363  '_raw' => $data,
364  ];
365 
366  if ( ! empty( $data['products'] ) ) {
367  foreach ( $data['products'] as $product ) {
368  if ( empty( $product['files'][0]['file'] ) || empty( $product['id'] ) || empty( $product['textdomain'] ) ) {
369  continue;
370  }
371 
372  $normalized_license_data['products'][ $product['id'] ] = [
373  'id' => $product['id'],
374  'text_domain' => $product['textdomain'],
375  'download' => $product['files'][0]['file']
376  ];
377  }
378  }
379 
380  if ( $multiple_licenses ) {
381  $normalized_response_data[ $license_key ] = $normalized_license_data;
382  } else {
383  $normalized_response_data = $normalized_license_data;
384  }
385  }
386 
387  return $normalized_response_data;
388  }
389 
390  /**
391  * Checks license key for validity.
392  *
393  * @since 1.0.0
394  *
395  * @param string $license_key
396  *
397  * @throws Exception
398  *
399  * @return array License data.
400  */
401  public function check_license( $license_key ) {
402  try {
403  return $this->perform_remote_license_call( $license_key, self::EDD_ACTION_CHECK_LICENSE );
404  } catch ( Exception $e ) {
405  throw new Exception( $e->getMessage() );
406  }
407  }
408 
409  /**
410  * Checks multiples license keys for validity.
411  *
412  * @since 1.0.0
413  *
414  * @param array $license_keys
415  *
416  * @throws Exception
417  *
418  * @return array Licenses data.
419  */
420  public function check_licenses( array $license_keys ) {
421  try {
422  return $this->perform_remote_license_call( $license_keys, self::EDD_ACTION_CHECK_LICENSE );
423  } catch ( Exception $e ) {
424  throw new Exception( $e->getMessage() );
425  }
426  }
427 
428  /**
429  * AJAX request wrapper for the activate_license() method.
430  *
431  * @since 1.0.0
432  *
433  * @param array $payload
434  *
435  * @throws Exception
436  *
437  * @return array License information.
438  */
439  public function ajax_activate_license( array $payload ) {
440  if ( ! Framework::get_instance()->current_user_can( 'manage_licenses' ) ) {
441  throw new Exception( esc_html__( 'You do not have a permission to perform this action.', 'gk-gravityview' ) );
442  }
443 
444  if ( empty( $payload['key'] ) ) {
445  throw new Exception( esc_html__( 'Missing license key.', 'gk-gravityview' ) );
446  }
447 
448  return $this->modify_license_data_for_frontend_output( $this->activate_license( $payload['key'] ) );
449  }
450 
451  /**
452  * Activates license.
453  *
454  * @since 1.0.0
455  *
456  * @param string $license_key license_key
457  *
458  * @throws Exception
459  *
460  * @return array License information.
461  */
462  public function activate_license( $license_key ) {
463  if ( ! Framework::get_instance()->current_user_can( 'manage_licenses' ) ) {
464  throw new Exception( esc_html__( 'You do not have a permission to perform this action.', 'gk-gravityview' ) );
465  }
466 
467  $licenses_data = $this->get_licenses_data();
468 
469  if ( isset( $licenses_data[ $license_key ] ) ) {
470  throw new Exception( esc_html__( 'This license is already activated.', 'gk-gravityview' ) );
471  }
472 
473  try {
474  $response = $this->perform_remote_license_call( $license_key, self::EDD_ACTION_ACTIVATE_LICENSE );
475 
476  if ( ! $response['_raw']['success'] ) {
477  throw new Exception( $this->get_license_key_status_message( $response['_raw']['error'] ) );
478  }
479 
480  if ( ! $response['_raw']['success'] ) {
481  throw new Exception( esc_html__( 'Could not get information on products associated with this license.', 'gk-gravityview' ) );
482  }
483  } catch ( Exception $e ) {
484  throw new Exception( $e->getMessage() );
485  }
486 
487  unset( $response['_raw'] );
488 
489  $licenses_data[ $license_key ] = $response;
490 
491  $this->save_licenses_data( $licenses_data );
492 
493  if ( CoreHelpers::is_network_admin() ) {
494  delete_site_transient( 'update_plugins ' );
495  } else {
496  delete_transient( 'update_plugins' );
497  }
498 
499  return $response;
500  }
501 
502  /**
503  * AJAX request wrapper for the deactivate_license() method.
504  *
505  * @since 1.0.0
506  *
507  * @param array $payload
508  *
509  * @throws Exception
510  *
511  * @return void
512  */
513  public function ajax_deactivate_license( array $payload ) {
514  $payload = wp_parse_args( $payload, [
515  'key' => false,
516  'force_removal' => false,
517  ] );
518 
519  if ( ! $payload['key'] ) {
520  throw new Exception( esc_html__( 'Missing license key.', 'gk-gravityview' ) );
521  }
522 
523  $licenses_data = $this->get_licenses_data();
524 
525  $license_key = Encryption::get_instance()->decrypt( $payload['key'] );
526 
527  if ( empty( $licenses_data[ $license_key ] ) ) {
528  throw new Exception( esc_html__( 'The license key is invalid.', 'gk-gravityview' ) );
529  }
530 
531  $this->deactivate_license( $license_key, (bool) $payload['force_removal'] );
532  }
533 
534  /**
535  * Deactivates license.
536  *
537  * @since 1.0.0
538  * @since 1.0.7 Added $force_removal parameter.
539  *
540  * @param string $license_key
541  * @param bool $force_removal (optional) Forces removal of license from the local licenses object even if deactivation request fails. Default: false.
542  *
543  * @throws Exception
544  *
545  * @return void
546  */
547  public function deactivate_license( $license_key, $force_removal = false ) {
548  $licenses_data = $this->get_licenses_data();
549 
550  try {
551  $response = $this->perform_remote_license_call( $license_key, self::EDD_ACTION_DEACTIVATE_LICENSE );
552 
553  if ( ! $force_removal && ! Arr::get( $response, '_raw.success' ) ) {
554  // Unsuccessful deactivation can happen when the license has expired, in which case we should treat it as a "success" and remove from our list.
555  // If the license hasn't expired, then there is a problem deactivating it, and we should throw an exception.
556  if ( ! Arr::get( $response, 'expiry' ) || ! $this->is_expired_license( Arr::get( $response, 'expiry' ) ) ) {
557  throw new Exception( esc_html__( 'Failed to deactivate license.', 'gk-gravityview' ) );
558  }
559  }
560  } catch ( Exception $e ) {
561  if ( ! $force_removal ) {
562  throw new Exception( $e->getMessage() );
563  }
564  }
565 
566  unset( $licenses_data[ $license_key ] );
567 
568  if ( CoreHelpers::is_network_admin() ) {
569  delete_site_transient( 'update_plugins ' );
570  } else {
571  delete_transient( 'update_plugins' );
572  }
573 
574  $this->save_licenses_data( $licenses_data );
575  }
576 
577  /**
578  * Adds additional data to the license object for use in the frontend.
579  * - Encrypts license key
580  * - Formats expiration date or message if license is expired
581  *
582  * @since 1.0.0
583  *
584  * @param $license
585  *
586  * @return array
587  */
588  public function modify_license_data_for_frontend_output( $license ) {
589  $expiry = ! empty( $license['expiry'] ) ? $license['expiry'] : 'invalid';
590  $expired = false;
591 
592  if ( ! ctype_alpha( $expiry ) ) {
593  $expired = $this->is_expired_license( $expiry );
594 
595  $expiry = $expired
596  ? human_time_diff( $expiry, current_time( 'timestamp' ) ) . ' ' . esc_html_x( 'ago', 'Indicates "time ago"', 'gk-gravityview' )
597  : date_i18n( get_option( 'date_format' ), $expiry );
598 
599  }
600 
601  try {
602  $encrypted_key = Encryption::get_instance()->encrypt( $license['key'], false, Core::get_request_unique_string() );
603  } catch ( Exception $e ) {
604  LoggerFramework::get_instance()->error( 'Failed to encrypt license key: ' . $e->getMessage() );
605 
606  $encrypted_key = 'key_encryption_failed';
607  }
608 
609  return array_merge( $license, [
610  'expiry' => $expiry,
611  'expired' => $expired,
612  'key' => $encrypted_key,
613  'masked_key' => $this->mask_license_key( $license['key'] )
614  ] );
615  }
616 
617  /**
618  * Masks part of the license key
619  *
620  * @since 1.0.0
621  *
622  * @param string $license_key
623  *
624  * @return string
625  */
626  public function mask_license_key( $license_key ) {
627  $length = strlen( $license_key );
628  $visible_count = (int) round( $length / 8 );
629  $hidden_count = $length - ( $visible_count * 4 );
630 
631  return sprintf( '%s%s%s',
632  substr( $license_key, 0, $visible_count ),
633  str_repeat( '✽', $hidden_count ),
634  substr( $license_key, ( $visible_count * -1 ), $visible_count )
635  );
636  }
637 
638  /**
639  * Saves new or removes existing hardcoded licenses from the license data.
640  *
641  * @since 1.0.0
642  *
643  * @return void
644  */
645  public function process_hardcoded_licenses() {
646  $hardcoded_license_keys = [];
647 
648  foreach ( self::HARDCODED_LICENSE_CONSTANTS as $constant ) {
649  if ( ! defined( $constant ) ) {
650  continue;
651  }
652 
653  if ( is_array( constant( $constant ) ) ) {
654  $hardcoded_license_keys = array_merge( $hardcoded_license_keys, constant( $constant ) );
655  } else {
656  $hardcoded_license_keys[] = constant( $constant );
657  }
658  }
659 
660  $licenses_data = $this->get_licenses_data();
661 
662  // Remove any licenses that are no longer hardcoded.
663  $removed_hardcoded_licenses = 0;
664 
665  foreach ( $licenses_data as $key => $license ) {
666  if ( ! empty( $license['hardcoded'] ) && ! in_array( $key, $hardcoded_license_keys, true ) ) {
667  $removed_hardcoded_licenses++;
668 
669  unset( $licenses_data[ $key ] );
670  }
671  }
672 
673  if ( $removed_hardcoded_licenses ) {
674  $this->save_licenses_data( $licenses_data );
675  }
676 
677  if ( empty( $hardcoded_license_keys ) ) {
678  return;
679  }
680 
681  // Add any new hardcoded licenses.
682  $license_keys_to_check = array_values( array_diff( $hardcoded_license_keys, array_keys( $licenses_data ) ) );
683 
684  if ( empty( $license_keys_to_check ) ) {
685  return;
686  }
687 
688  try {
689  $checked_licenses = $this->check_licenses( $license_keys_to_check );
690  } catch ( Exception $e ) {
691  LoggerFramework::get_instance()->error( "Failed to check hardcoded licenses. {$e->getMessage()}." );
692 
693  return;
694  }
695 
696  foreach ( $checked_licenses as $key => $license ) {
697  if ( ! $license['_raw']['success'] ) {
698  LoggerFramework::get_instance()->warning( "Hardcoded license {$key} is invalid." );
699 
700  continue;
701  }
702 
703  unset( $license['_raw'] );
704 
705  $license['hardcoded'] = true;
706 
707  $licenses_data[ $key ] = $license;
708  }
709 
710  $this->save_licenses_data( $licenses_data );
711  }
712 
713  /**
714  * Migrates licenses for products that do not have Foundation integrated.
715  *
716  * @since 1.0.0
717  *
718  * @param bool $force_migration Whether to force migration even if it was done before.
719  *
720  * @return void
721  */
722  public function migrate_legacy_licenses( $force_migration = false ) {
723  $logger = LoggerFramework::get_instance();
724 
725  $migration_status_id = Framework::ID . '/legacy-licenses-migrated';
726 
727  $save_migration_status_in_db = function () use ( $migration_status_id ) {
728  update_site_option( $migration_status_id, current_time( 'timestamp' ) );
729  };
730 
731  if ( get_site_option( $migration_status_id ) && ! $force_migration ) {
732  return;
733  }
734 
735  $licenses_data = $this->get_licenses_data();
736 
737  $license_keys_to_migrate = [];
738 
739  $db_options = [
740  'gravityformsaddon_gravityview-importer_settings',
741  'gravityformsaddon_gravityview_app_settings',
742  'gravityformsaddon_gravityview-inline-edit_settings',
743  'gravityformsaddon_gravitycharts_settings',
744  'gravityformsaddon_gk-gravityactions_settings',
745  'gravityformsaddon_gravityview-calendar_settings',
746  'gravityformsaddon_gravityexport_settings',
747  'gravityformsaddon_gravityview-entry-revisions_settings',
748  ];
749 
750  foreach ( $db_options as $option ) {
751  $license = Arr::get( get_option( $option, [] ), 'license_key' );
752 
753  $option = str_replace( [ 'gravityformsaddon_', '_settings' ], '', $option );
754 
755  if ( $license ) {
756  $license_keys_to_migrate[ $license ] = $option;
757  } else {
758  $logger->warning( "Legacy license not found for {$option}." );
759  }
760  }
761 
762  if ( empty( $license_keys_to_migrate ) ) {
763  $save_migration_status_in_db();
764 
765  $logger->info( 'Did not find any legacy licenses to migrate.' );
766 
767  return;
768  }
769 
770  try {
771  $checked_licenses = $this->check_licenses( array_keys( $license_keys_to_migrate ) );
772  } catch ( Exception $e ) {
773  $logger->error( "Failed to check legacy licenses. {$e->getMessage()}." );
774 
775  return;
776  }
777 
778  foreach ( $checked_licenses as $key => $license ) {
779  if ( ! $license['_raw']['success'] ) {
780  $logger->warning( "Legacy license {$key} is invalid." );
781 
782  continue;
783  }
784 
785  try {
786  $license = $this->activate_license( $key );
787  } catch ( Exception $e ) {
788  $logger->error( "Failed to activate legacy license {$key}. {$e->getMessage()}." );
789 
790  continue;
791  }
792 
793  $logger->info( "Migrated legacy license for {$license_keys_to_migrate[$key]}." );
794 
795  $licenses_data[ $key ] = $license;
796  }
797 
798  $save_migration_status_in_db();
799 
800  $this->save_licenses_data( $licenses_data );
801  }
802 
803  /**
804  * Rechecks all licenses and updates the database.
805  *
806  * @since 1.0.0
807  *
808  * @param bool $skip_cache Whether to skip returning products from cache.
809  *
810  * @return void
811  */
812  public function recheck_all_licenses( $skip_cache = false ) {
813  $cache_id = Framework::ID . '/licenses';
814 
815  $last_validation = get_site_transient( $cache_id );
816 
817  if ( $last_validation && ! $skip_cache ) {
818  return;
819  }
820 
821  $licenses_data = $this->get_licenses_data();
822 
823  $revalidated_licenses = [];
824 
825  if ( empty( $licenses_data ) ) {
826  return;
827  }
828 
829  try {
830  $license_check_result = $this->check_licenses( array_keys( $licenses_data ) );
831 
832  foreach ( $license_check_result as $key => $license ) {
833  if ( ! $license['_raw']['success'] ) {
834  LoggerFramework::get_instance()->warning( "License {$key} is invalid." );
835 
836  continue;
837  }
838 
839  unset( $license['_raw'] );
840 
841  if ( ! empty( $licenses_data[ $key ]['hardcoded'] ) ) {
842  $license['hardcoded'] = true;
843  }
844 
845  $revalidated_licenses[ $key ] = $license;
846  }
847  } catch ( Exception $e ) {
848  LoggerFramework::get_instance()->error( "Failed to revalidate all licenses. {$e->getMessage()}." );
849  }
850 
851  set_site_transient( $cache_id, current_time( 'timestamp' ), DAY_IN_SECONDS );
852 
853  if ( ! empty( $revalidated_licenses ) ) {
854  $this->save_licenses_data( $revalidated_licenses );
855  }
856  }
857 
858  /**
859  * Optionally adds notices to installed plugins when license is invalid or expired.
860  *
861  * @since 1.0.0
862  *
863  * @param string $plugin_name
864  * @param array $plugin_data
865  *
866  * @return void
867  */
868  public function display_license_info_on_plugins_page( $plugin_name, $plugin_data ) {
869  static $products_data;
870 
871  if ( is_multisite() && ! CoreHelpers::is_network_admin() ) {
872  return;
873  }
874 
875  $is_active = CoreHelpers::is_network_admin() ? is_plugin_active_for_network( $plugin_name ) : is_plugin_active( $plugin_name );
876 
877  if ( ! $is_active ) {
878  return;
879  }
880 
881  $licenses_data = $this->get_licenses_data();
882 
883  if ( ! $products_data ) {
884  try {
885  $products_data = ProductManager::get_instance()->get_products_data( [ 'key_by' => 'text_domain' ] );
886  } catch ( Exception $e ) {
887  LoggerFramework::get_instance()->error( "Failed to get products on the plugins page. {$e->getMessage()}." );
888 
889  return;
890  }
891  }
892 
893  if ( ! isset( $products_data[ $plugin_data['TextDomain'] ] ) ) {
894  return;
895  }
896 
897  $this_plugin = $products_data[ $plugin_data['TextDomain'] ];
898 
899  $valid_licenses = [];
900 
901  foreach ( $this_plugin['licenses'] as $license_key ) {
902  if ( ! isset( $licenses_data[ $license_key ] ) ) {
903  continue;
904  }
905 
906  $valid_licenses[] = $license_key;
907  }
908 
909  if ( ! empty( $valid_licenses ) ) {
910  return;
911  }
912 
913  add_filter( "after_plugin_row_{$plugin_name}", function ( $plugin_name, $plugin_data ) use ( $this_plugin ) {
914  $url = Framework::get_instance()->get_link_to_product_search( $this_plugin['id'] );
915 
916  $message = strtr(
917  esc_html_x( 'This is an unlicensed product. Please [link]visit the licensing page[/link] to enter a valid license or to purchase a new one.', 'Placeholders inside [] are not to be translated.', 'gk-gravityview' ),
918  [
919  '[link]' => '<a href="' . $url . '">',
920  '[/link]' => '</a>'
921  ]
922  );
923 
924  $screen = get_current_screen();
925  $columns = get_column_headers( $screen );
926  $colspan = ! is_countable( $columns ) ? 3 : count( $columns );
927  $plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_data['Name'] );
928  $plugin_name = $plugin_data['Name'];
929 
930  echo <<<HTML
931 <tr class="plugin-update-tr active gk-custom-plugin-update-message" data-slug="{$plugin_slug}11" data-plugin="{$plugin_name}">
932  <td colspan="{$colspan}" class="plugin-update colspanchange">
933  <div class="update-message notice inline notice-error notice-alt">
934  <p>{$message}</p>
935  </div>
936  </td>
937 </tr>
938 <style>tr[data-slug="{$plugin_slug}"]:not(.gk-custom-plugin-update-message) td, tr[data-slug="{$plugin_slug}"]:not(.gk-custom-plugin-update-message) th { box-shadow: none !important; }</style>
939 HTML;
940  }, 11, 2 );
941  }
942 
943  /**
944  * Retrieves site data (plugin versions, integrations, etc.) to be sent along with the license check.
945  *
946  * @since 1.0.0
947  *
948  * @return array
949  */
950  public function get_site_data() {
951  global $wpdb;
952 
953  $data = [];
954 
955  $theme_data = wp_get_theme();
956  $theme = $theme_data->Name . ' ' . $theme_data->Version;
957 
958  $data['php_version'] = PHP_VERSION;
959  $data['wp_version'] = get_bloginfo( 'version' );
960  $data['mysql_version'] = $wpdb->db_version();
961 
962  if ( defined( 'GV_PLUGIN_VERSION' ) ) {
963  $data['gv_version'] = GV_PLUGIN_VERSION;
964  }
965 
966  if ( class_exists( 'GFForms' ) ) {
967  $data['gf_version'] = GFForms::$version;
968  }
969 
970  if ( isset( $_SERVER['SERVER_SOFTWARE'] ) ) {
971  $data['server'] = $_SERVER['SERVER_SOFTWARE'];
972  }
973 
974  $data['multisite'] = is_multisite();
975  $data['theme'] = $theme;
976  $data['url'] = is_multisite() ? network_home_url() : home_url();
977  $data['beta'] = SettingsFramework::get_instance()->get_plugin_setting( Core::ID, 'beta' );
978 
979  // GravityView view data.
980  $gravityview_posts = wp_count_posts( 'gravityview', 'readable' );
981 
982  $data['view_count'] = null;
983  $data['view_first'] = null;
984  $data['view_latest'] = null;
985 
986  if ( ! empty( $gravityview_posts->publish ) ) {
987  $data['view_count'] = $gravityview_posts->publish;
988 
989  $first = get_posts( 'numberposts=1&post_type=gravityview&post_status=publish&order=ASC' );
990  $latest = get_posts( 'numberposts=1&post_type=gravityview&post_status=publish&order=DESC' );
991 
992  if ( $first = array_shift( $first ) ) {
993  $data['view_first'] = $first->post_date;
994  }
995  if ( $latest = array_pop( $latest ) ) {
996  $data['view_latest'] = $latest->post_date;
997  }
998  }
999 
1000  // Gravity Forms form data.
1001  if ( class_exists( 'GFFormsModel' ) ) {
1002  $form_data = GFFormsModel::get_form_count();
1003 
1004  $data['forms_total'] = $form_data['total'];
1005  $data['forms_active'] = $form_data['active'];
1006  $data['forms_inactive'] = $form_data['inactive'];
1007  $data['forms_trash'] = $form_data['trash'];
1008  }
1009 
1010  $plugins = CoreHelpers::get_installed_plugins();
1011  foreach ( $plugins as &$plugin ) {
1012  $plugin = Arr::only( $plugin, [ 'name', 'version', 'active', 'network_activated' ] );
1013  $plugin = array_filter( $plugin ); // Don't include active/network activated if false
1014  }
1015 
1016  $data['plugins'] = $plugins;
1017  $data['locale'] = get_locale();
1018 
1019  return $data;
1020  }
1021 
1022  /**
1023  * Optionally updates the Licenses submenu badge count if any of the products are unlicensed.
1024  *
1025  * @since 1.0.0
1026  *
1027  * @return void
1028  */
1029  public function update_submenu_badge_count() {
1030  if ( ! Framework::get_instance()->current_user_can( 'manage_licenses' ) ) {
1031  return;
1032  }
1033 
1034  try {
1035  $products_data = ProductManager::get_instance()->get_products_data();
1036  } catch ( Exception $e ) {
1037  LoggerFramework::get_instance()->warning( 'Unable to get products when adding a badge count for unlicensed products.' );
1038 
1039  return;
1040  }
1041 
1042  $update_count = 0;
1043 
1044  foreach ( $products_data as $product ) {
1045  if ( $product['active'] && empty( $product['licenses'] ) ) {
1046  $update_count++;
1047  }
1048  }
1049 
1050  if ( ! $update_count ) {
1051  return;
1052  }
1053 
1054  add_filter( 'gk/foundation/admin-menu/submenu/' . Framework::ID . '/counter', function ( $count ) use ( $update_count ) {
1055  return (int) $count + $update_count;
1056  } );
1057  }
1058 
1059  /**
1060  * Determines if the license has expired.
1061  *
1062  * @since 1.0.0
1063  *
1064  * @param int|string $expiry Unix time or 'lifetime'.
1065  *
1066  * @return bool
1067  */
1068  public function is_expired_license( $expiry ) {
1069  if ( 'lifetime' === $expiry ) {
1070  return false;
1071  }
1072 
1073  return $expiry < current_time( 'timestamp' );
1074  }
1075 }
is_expired_license( $expiry)
Determines if the license has expired.
$url
Definition: post_image.php:25
const GV_PLUGIN_VERSION(! GravityKit\GravityView\Foundation\meets_min_php_version_requirement(__FILE__, '7.2.0'))
Constants.
Definition: gravityview.php:34
ajax_get_licenses_data(array $payload)
AJAX request wrapper for the get_licenses_data() method.
recheck_all_licenses( $skip_cache=false)
Rechecks all licenses and updates the database.
check_license( $license_key)
Checks license key for validity.
ajax_deactivate_license(array $payload)
AJAX request wrapper for the deactivate_license() method.
process_hardcoded_licenses()
Saves new or removes existing hardcoded licenses from the license data.
static get( $array, $key, $default=null)
{}
Definition: Arr.php:99
get_site_data()
Retrieves site data (plugin versions, integrations, etc.) to be sent along with the license check...
ajax_activate_license(array $payload)
AJAX request wrapper for the activate_license() method.
get_license_key_status_message( $status)
Returns license status message based on the EDD status code.
static get_instance( $secret_key='')
Returns class instance.
Definition: Encryption.php:67
display_license_info_on_plugins_page( $plugin_name, $plugin_data)
Optionally adds notices to installed plugins when license is invalid or expired.
modify_license_data_for_frontend_output( $license)
Adds additional data to the license object for use in the frontend.
save_licenses_data(array $licenses_data)
Saves license data in the database.
check_licenses(array $license_keys)
Checks multiples license keys for validity.
mask_license_key( $license_key)
Masks part of the license key.
static query_api( $url, array $args=[])
Performs remote call to GravityKit&#39;s EDD API.
migrate_legacy_licenses( $force_migration=false)
Migrates licenses for products that do not have Foundation integrated.
perform_remote_license_call( $license, $edd_action)
Performs remote call to the EDD API.
$update_count
update_submenu_badge_count()
Optionally updates the Licenses submenu badge count if any of the products are unlicensed.
get_product_licenses( $id)
Returns licenses for a product.
get_product_license_map( $key_by='id')
Returns an object keyed by product ID and associated licenses.
get_licenses_data()
Retrieves license data from the database.
configure_ajax_routes(array $routes)
Configures AJAX routes handled by this class.
static only( $array, $keys)
{}
Definition: Arr.php:180
static get_request_unique_string()
Returns a unique value that was generated for this request.
Definition: Core.php:693
deactivate_license( $license_key, $force_removal=false)
Deactivates license.