PluginProbe ʕ •ᴥ•ʔ
ShopEngine Elementor WooCommerce Builder Addon – All in One WooCommerce Solution / 4.7.2
ShopEngine Elementor WooCommerce Builder Addon – All in One WooCommerce Solution v4.7.2
4.9.1 4.9.0 2.0.0 2.1.0 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.1 3.0.0 3.1.0 3.1.1 4.0.0 4.0.1 4.1.0 4.1.1 4.2.0 4.2.1 4.3.0 4.3.1 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.6.7 4.6.8 4.6.9 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 4.7.9 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.8.9 trunk 0.1.2-beta 0.1.3-beta 0.1.4-beta 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.5.0 1.5.1 1.6.0 1.6.1 1.7.0 1.8.0 1.8.1 1.9.0
shopengine / libs / updater / plugin-updater.php
shopengine / libs / updater Last commit date
init.php 4 years ago plugin-updater.php 1 year ago
plugin-updater.php
713 lines
1 <?php
2
3 namespace ShopEngine\Libs\Updater;
4
5 use ShopEngine\Utils\Helper;
6
7 defined('ABSPATH') || exit;
8
9 /**
10 * Allows plugins to use their own update API.
11 *
12 * @author Easy Digital Downloads
13 * @version 1.9.4
14 */
15 class Plugin_Updater {
16
17 private $api_url = '';
18 private $api_data = array();
19 private $plugin_file = '';
20 private $name = '';
21 private $slug = '';
22 private $version = '';
23 private $wp_override = false;
24 private $beta = false;
25 private $failed_request_cache_key;
26
27 private $health_check_timeout = 15;
28 private $update_check_interval = '5 hours';
29
30 /**
31 * Class constructor.
32 *
33 * @uses plugin_basename()
34 * @uses hook()
35 *
36 * @param string $_api_url The URL pointing to the custom API endpoint.
37 * @param string $_plugin_file Path to the plugin file.
38 * @param array $_api_data Optional data to send with API calls.
39 */
40 public function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
41
42 global $edd_plugin_data;
43
44 $this->api_url = trailingslashit( $_api_url );
45 $this->api_data = $_api_data;
46 $this->plugin_file = $_plugin_file;
47 $this->name = plugin_basename( $_plugin_file );
48 $this->slug = basename( dirname( $_plugin_file ) );
49 $this->version = $_api_data['version'];
50 $this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
51 $this->beta = ! empty( $this->api_data['beta'] ) ? true : false;
52 $this->failed_request_cache_key = 'edd_sl_failed_http_' . md5( $this->api_url );
53
54 $edd_plugin_data[ $this->slug ] = $this->api_data;
55
56 /**
57 * Fires after the $edd_plugin_data is setup.
58 *
59 * @since x.x.x
60 *
61 * @param array $edd_plugin_data Array of EDD SL plugin data.
62 */
63 do_action( 'post_edd_sl_plugin_updater_setup', $edd_plugin_data );
64
65 // Set up hooks.
66 $this->init();
67 }
68
69 /**
70 * Set up WordPress filters to hook into WP's update process.
71 *
72 * @uses add_filter()
73 *
74 * @return void
75 */
76 public function init() {
77
78 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
79 add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
80 add_action( 'after_plugin_row', array( $this, 'show_update_notification' ), 10, 2 );
81 add_action( 'admin_init', array( $this, 'show_changelog' ) );
82 }
83
84 /**
85 * Check for Updates at the defined API endpoint and modify the update array.
86 *
87 * This function dives into the update API just when WordPress creates its update array,
88 * then adds a custom API call and injects the custom plugin data retrieved from the API.
89 * It is reassembled from parts of the native WordPress plugin update code.
90 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
91 *
92 * @uses api_request()
93 *
94 * @param array $_transient_data Update array build by WordPress.
95 * @return array Modified update array with custom plugin data.
96 */
97 public function check_update( $_transient_data ) {
98
99 if ( ! is_object( $_transient_data ) ) {
100 $_transient_data = new \stdClass();
101 }
102
103 if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
104 return $_transient_data;
105 }
106
107 $current = $this->get_update_transient_data();
108 if ( false !== $current && is_object( $current ) && isset( $current->new_version ) ) {
109 if ( version_compare( $this->version, $current->new_version, '<' ) ) {
110 $_transient_data->response[ $this->name ] = $current;
111 } else {
112 // Populating the no_update information is required to support auto-updates in WordPress 5.5.
113 $_transient_data->no_update[ $this->name ] = $current;
114 }
115 }
116 $_transient_data->last_checked = time();
117 $_transient_data->checked[ $this->name ] = $this->version;
118
119 return $_transient_data;
120 }
121
122 /**
123 * Get repo API data from store.
124 * Save to cache.
125 *
126 * @return \stdClass
127 */
128 public function get_repo_api_data() {
129 $version_info = $this->get_cached_version_info();
130
131 if ( false === $version_info ) {
132 $version_info = $this->api_request(
133 'plugin_latest_version',
134 array(
135 'slug' => $this->slug,
136 'beta' => $this->beta,
137 )
138 );
139 if ( ! $version_info ) {
140 return false;
141 }
142
143 // This is required for your plugin to support auto-updates in WordPress 5.5.
144 $version_info->plugin = $this->name;
145 $version_info->id = $this->name;
146 $version_info->tested = $this->get_tested_version( $version_info );
147 if ( ! isset( $version_info->requires ) ) {
148 $version_info->requires = '';
149 }
150 if ( ! isset( $version_info->requires_php ) ) {
151 $version_info->requires_php = '';
152 }
153
154 $this->set_version_info_cache( $version_info );
155 }
156
157 return $version_info;
158 }
159
160 /**
161 * Gets a limited set of data from the API response.
162 * This is used for the update_plugins transient.
163 *
164 * @since 3.8.12
165 * @return \stdClass|false
166 */
167 private function get_update_transient_data() {
168 $version_info = $this->get_repo_api_data();
169
170 if ( ! $version_info ) {
171 return false;
172 }
173
174 $limited_data = new \stdClass();
175 $limited_data->slug = $this->slug;
176 $limited_data->plugin = $this->name;
177 $limited_data->url = $version_info->url;
178 $limited_data->package = $version_info->package;
179 $limited_data->icons = $this->convert_object_to_array( $version_info->icons );
180 $limited_data->banners = $this->convert_object_to_array( $version_info->banners );
181 $limited_data->new_version = $version_info->new_version;
182 $limited_data->tested = isset($response->records) ? $version_info->tested : '';
183 $limited_data->requires = isset($response->records) ? $version_info->requires : '';
184 $limited_data->requires_php = isset($response->records) ? $version_info->requires_php : '';
185
186 return $limited_data;
187 }
188
189 /**
190 * Gets the plugin's tested version.
191 *
192 * @since 1.9.2
193 * @param object $version_info
194 * @return null|string
195 */
196 private function get_tested_version( $version_info ) {
197
198 // There is no tested version.
199 if ( empty( $version_info->tested ) ) {
200 return null;
201 }
202
203 // Strip off extra version data so the result is x.y or x.y.z.
204 list( $current_wp_version ) = explode( '-', get_bloginfo( 'version' ) );
205
206 // The tested version is greater than or equal to the current WP version, no need to do anything.
207 if ( version_compare( $version_info->tested, $current_wp_version, '>=' ) ) {
208 return $version_info->tested;
209 }
210 $current_version_parts = explode( '.', $current_wp_version );
211 $tested_parts = explode( '.', $version_info->tested );
212
213 // The current WordPress version is x.y.z, so update the tested version to match it.
214 if ( isset( $current_version_parts[2] ) && $current_version_parts[0] === $tested_parts[0] && $current_version_parts[1] === $tested_parts[1] ) {
215 $tested_parts[2] = $current_version_parts[2];
216 }
217
218 return implode( '.', $tested_parts );
219 }
220
221 /**
222 * Show the update notification on multisite subsites.
223 *
224 * @param string $file
225 * @param array $plugin
226 */
227 public function show_update_notification( $file, $plugin ) {
228
229 // Return early if in the network admin, or if this is not a multisite install.
230 if ( is_network_admin() || ! is_multisite() ) {
231 return;
232 }
233
234 // Allow single site admins to see that an update is available.
235 if ( ! current_user_can( 'activate_plugins' ) ) {
236 return;
237 }
238
239 if ( $this->name !== $file ) {
240 return;
241 }
242
243 // Do not print any message if update does not exist.
244 $update_cache = get_site_transient( 'update_plugins' );
245
246 if ( ! isset( $update_cache->response[ $this->name ] ) ) {
247 if ( ! is_object( $update_cache ) ) {
248 $update_cache = new \stdClass();
249 }
250 $update_cache->response[ $this->name ] = $this->get_repo_api_data();
251 }
252
253 // Return early if this plugin isn't in the transient->response or if the site is running the current or newer version of the plugin.
254 if ( empty( $update_cache->response[ $this->name ] ) || version_compare( $this->version, $update_cache->response[ $this->name ]->new_version, '>=' ) ) {
255 return;
256 }
257
258 printf(
259 '<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">',
260 $this->slug,
261 $file,
262 in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive'
263 );
264
265 echo '<td colspan="3" class="plugin-update colspanchange">';
266 echo '<div class="update-message notice inline notice-warning notice-alt"><p>';
267
268 $changelog_link = '';
269 if ( ! empty( $update_cache->response[ $this->name ]->sections->changelog ) ) {
270 $changelog_link = add_query_arg(
271 array(
272 'edd_sl_action' => 'view_plugin_changelog',
273 'plugin' => urlencode( $this->name ),
274 'slug' => urlencode( $this->slug ),
275 'TB_iframe' => 'true',
276 'width' => 77,
277 'height' => 911,
278 ),
279 self_admin_url( 'index.php' )
280 );
281 }
282 $update_link = add_query_arg(
283 array(
284 'action' => 'upgrade-plugin',
285 'plugin' => urlencode( $this->name ),
286 ),
287 self_admin_url( 'update.php' )
288 );
289
290 printf(
291 /* translators: the plugin name. */
292 esc_html__( 'There is a new version of %1$s available.', 'shopengine' ),
293 esc_html( $plugin['Name'] )
294 );
295
296 if ( ! current_user_can( 'update_plugins' ) ) {
297 echo ' ';
298 esc_html_e( 'Contact your network administrator to install the update.', 'shopengine' );
299 } elseif ( empty( $update_cache->response[ $this->name ]->package ) && ! empty( $changelog_link ) ) {
300 echo ' ';
301 printf(
302 /* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate. */
303 __( '%1$sView version %2$s details%3$s.', 'shopengine' ),
304 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
305 esc_html( $update_cache->response[ $this->name ]->new_version ),
306 '</a>'
307 );
308 } elseif ( ! empty( $changelog_link ) ) {
309 echo ' ';
310 printf(
311 __( '%1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'shopengine' ),
312 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
313 esc_html( $update_cache->response[ $this->name ]->new_version ),
314 '</a>',
315 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
316 '</a>'
317 );
318 } else {
319 printf(
320 ' %1$s%2$s%3$s',
321 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
322 esc_html__( 'Update now.', 'shopengine' ),
323 '</a>'
324 );
325 }
326
327 do_action( "in_plugin_update_message-{$file}", $plugin, $plugin );
328
329 echo '</p></div></td></tr>';
330 }
331
332 /**
333 * Gets the plugins active in a multisite network.
334 *
335 * @return array
336 */
337 private function get_active_plugins() {
338 $active_plugins = (array) get_option( 'active_plugins' );
339 $active_network_plugins = (array) get_site_option( 'active_sitewide_plugins' );
340
341 return array_merge( $active_plugins, array_keys( $active_network_plugins ) );
342 }
343
344 /**
345 * Updates information on the "View version x.x details" page with custom data.
346 *
347 * @uses api_request()
348 *
349 * @param mixed $_data
350 * @param string $_action
351 * @param object $_args
352 * @return object $_data
353 */
354 public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
355
356 if ( 'plugin_information' !== $_action ) {
357
358 return $_data;
359
360 }
361
362 if ( ! isset( $_args->slug ) || ( $_args->slug !== $this->slug ) ) {
363
364 return $_data;
365
366 }
367
368 $to_send = array(
369 'slug' => $this->slug,
370 'is_ssl' => is_ssl(),
371 'fields' => array(
372 'banners' => array(),
373 'reviews' => false,
374 'icons' => array(),
375 ),
376 );
377
378 // Get the transient where we store the api request for this plugin for 24 hours
379 $edd_api_request_transient = $this->get_cached_version_info();
380
381 //If we have no transient-saved value, run the API, set a fresh transient with the API value, and return that value too right now.
382 if ( empty( $edd_api_request_transient ) ) {
383
384 $api_response = $this->api_request( 'plugin_information', $to_send );
385
386 // Expires in 3 hours
387 $this->set_version_info_cache( $api_response );
388
389 if ( false !== $api_response ) {
390 $_data = $api_response;
391 }
392 } else {
393 $_data = $edd_api_request_transient;
394 }
395
396 // Convert sections into an associative array, since we're getting an object, but Core expects an array.
397 if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
398 $_data->sections = $this->convert_object_to_array( $_data->sections );
399 }
400
401 // Convert banners into an associative array, since we're getting an object, but Core expects an array.
402 if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
403 $_data->banners = $this->convert_object_to_array( $_data->banners );
404 }
405
406 // Convert icons into an associative array, since we're getting an object, but Core expects an array.
407 if ( isset( $_data->icons ) && ! is_array( $_data->icons ) ) {
408 $_data->icons = $this->convert_object_to_array( $_data->icons );
409 }
410
411 // Convert contributors into an associative array, since we're getting an object, but Core expects an array.
412 if ( isset( $_data->contributors ) && ! is_array( $_data->contributors ) ) {
413 $_data->contributors = $this->convert_object_to_array( $_data->contributors );
414 }
415
416 if ( ! isset( $_data->plugin ) ) {
417 $_data->plugin = $this->name;
418 }
419
420 if ( ! isset( $_data->version ) && ! empty( $_data->new_version ) ) {
421 $_data->version = $_data->new_version;
422 }
423
424 return $_data;
425 }
426
427 /**
428 * Convert some objects to arrays when injecting data into the update API
429 *
430 * Some data like sections, banners, and icons are expected to be an associative array, however due to the JSON
431 * decoding, they are objects. This method allows us to pass in the object and return an associative array.
432 *
433 * @since 3.6.5
434 *
435 * @param stdClass $data
436 *
437 * @return array
438 */
439 private function convert_object_to_array( $data ) {
440 if ( ! is_array( $data ) && ! is_object( $data ) ) {
441 return array();
442 }
443 $new_data = array();
444 foreach ( $data as $key => $value ) {
445 $new_data[ $key ] = is_object( $value ) ? $this->convert_object_to_array( $value ) : $value;
446 }
447
448 return $new_data;
449 }
450
451 /**
452 * Disable SSL verification in order to prevent download update failures
453 *
454 * @param array $args
455 * @param string $url
456 * @return object $array
457 */
458 public function http_request_args( $args, $url ) {
459
460 if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
461 $args['sslverify'] = $this->verify_ssl();
462 }
463 return $args;
464 }
465
466 /**
467 * Calls the API and, if successfull, returns the object delivered by the API.
468 *
469 * @uses get_bloginfo()
470 * @uses wp_remote_post()
471 * @uses is_wp_error()
472 *
473 * @param string $_action The requested action.
474 * @param array $_data Parameters for the API action.
475 * @return false|object|void
476 */
477 private function api_request( $_action, $_data ) {
478 $data = array_merge( $this->api_data, $_data );
479
480 if ( $data['slug'] !== $this->slug ) {
481 return;
482 }
483
484 // Don't allow a plugin to ping itself
485 if ( trailingslashit( home_url() ) === $this->api_url ) {
486 return false;
487 }
488
489 if ( $this->request_recently_failed() ) {
490 return false;
491 }
492
493 return $this->get_version_from_remote();
494 }
495
496 /**
497 * Determines if a request has recently failed.
498 *
499 * @since 1.9.1
500 *
501 * @return bool
502 */
503 private function request_recently_failed() {
504 $failed_request_details = get_option( $this->failed_request_cache_key );
505
506 // Request has never failed.
507 if ( empty( $failed_request_details ) || ! is_numeric( $failed_request_details ) ) {
508 return false;
509 }
510
511 /*
512 * Request previously failed, but the timeout has expired.
513 * This means we're allowed to try again.
514 */
515 if ( time() > $failed_request_details ) {
516 delete_option( $this->failed_request_cache_key );
517
518 return false;
519 }
520
521 return true;
522 }
523
524 /**
525 * Logs a failed HTTP request for this API URL.
526 * We set a timestamp for 1 hour from now. This prevents future API requests from being
527 * made to this domain for 1 hour. Once the timestamp is in the past, API requests
528 * will be allowed again. This way if the site is down for some reason we don't bombard
529 * it with failed API requests.
530 *
531 * @see EDD_SL_Plugin_Updater::request_recently_failed
532 *
533 * @since 1.9.1
534 */
535 private function log_failed_request() {
536 update_option( $this->failed_request_cache_key, strtotime( '+1 hour' ) );
537 }
538
539 /**
540 * If available, show the changelog for sites in a multisite install.
541 */
542 public function show_changelog() {
543
544 if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
545 return;
546 }
547
548 if ( empty( $_REQUEST['plugin'] ) ) {
549 return;
550 }
551
552 if ( empty( $_REQUEST['slug'] ) || $this->slug !== $_REQUEST['slug'] ) {
553 return;
554 }
555
556 if ( ! current_user_can( 'update_plugins' ) ) {
557 wp_die( esc_html__( 'You do not have permission to install plugin updates', 'shopengine' ), esc_html__( 'Error', 'shopengine' ), array( 'response' => 403 ) );
558 }
559
560 $version_info = $this->get_repo_api_data();
561 if ( isset( $version_info->sections ) ) {
562 $sections = $this->convert_object_to_array( $version_info->sections );
563 if ( ! empty( $sections['changelog'] ) ) {
564 echo '<div style="background:#fff;padding:10px;">' . wp_kses_post( $sections['changelog'] ) . '</div>';
565 }
566 }
567
568 exit;
569 }
570
571 /**
572 * Gets the current version information from the remote site.
573 *
574 * @return array|false
575 */
576 private function get_version_from_remote() {
577 $api_params = array(
578 'edd_action' => 'get_version',
579 'license' => ! empty( $this->api_data['license'] ) ? $this->api_data['license'] : '',
580 'item_name' => isset( $this->api_data['item_name'] ) ? $this->api_data['item_name'] : false,
581 'item_id' => isset( $this->api_data['item_id'] ) ? $this->api_data['item_id'] : false,
582 'version' => isset( $this->api_data['version'] ) ? $this->api_data['version'] : false,
583 'slug' => $this->slug,
584 'author' => $this->api_data['author'],
585 'url' => home_url(),
586 'beta' => $this->beta,
587 'php_version' => phpversion(),
588 'wp_version' => get_bloginfo( 'version' ),
589 );
590
591 /**
592 * Filters the parameters sent in the API request.
593 *
594 * @param array $api_params The array of data sent in the request.
595 * @param array $this->api_data The array of data set up in the class constructor.
596 * @param string $this->plugin_file The full path and filename of the file.
597 */
598 $api_params = apply_filters( 'edd_sl_plugin_updater_api_params', $api_params, $this->api_data, $this->plugin_file );
599
600 $request = wp_remote_post(
601 $this->api_url,
602 array(
603 'timeout' => $this->health_check_timeout,
604 'sslverify' => $this->verify_ssl(),
605 'body' => $api_params,
606 )
607 );
608
609 if ( is_wp_error( $request ) || ( 200 !== wp_remote_retrieve_response_code( $request ) ) ) {
610 $this->log_failed_request();
611
612 return false;
613 }
614
615 $request = json_decode( wp_remote_retrieve_body( $request ) );
616
617 if ( $request && isset( $request->sections ) ) {
618 $request->sections = maybe_unserialize( $request->sections );
619 } else {
620 $request = false;
621 }
622
623 if ( $request && isset( $request->banners ) ) {
624 $request->banners = maybe_unserialize( $request->banners );
625 }
626
627 if ( $request && isset( $request->icons ) ) {
628 $request->icons = maybe_unserialize( $request->icons );
629 }
630
631 if ( ! empty( $request->sections ) ) {
632 foreach ( $request->sections as $key => $section ) {
633 $request->$key = (array) $section;
634 }
635 }
636
637 return $request;
638 }
639
640 /**
641 * Get the version info from the cache, if it exists.
642 *
643 * @param string $cache_key
644 * @return object
645 */
646 public function get_cached_version_info( $cache_key = '' ) {
647
648 if ( empty( $cache_key ) ) {
649 $cache_key = $this->get_cache_key();
650 }
651
652 $cache = get_option( $cache_key );
653
654 // Cache is expired
655 if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
656 return false;
657 }
658
659 // We need to turn the icons into an array, thanks to WP Core forcing these into an object at some point.
660 $cache['value'] = json_decode( $cache['value'] );
661 if ( ! empty( $cache['value']->icons ) ) {
662 $cache['value']->icons = (array) $cache['value']->icons;
663 }
664
665 return $cache['value'];
666 }
667
668 /**
669 * Adds the plugin version information to the database.
670 *
671 * @param string $value
672 * @param string $cache_key
673 */
674 public function set_version_info_cache( $value = '', $cache_key = '' ) {
675
676 if ( empty( $cache_key ) ) {
677 $cache_key = $this->get_cache_key();
678 }
679
680 $data = array(
681 'timeout' => strtotime( $this->update_check_interval, time() ),
682 'value' => wp_json_encode( $value ),
683 );
684
685 update_option( $cache_key, $data, 'no' );
686
687 // Delete the duplicate option
688 delete_option( 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ) );
689 }
690
691 /**
692 * Returns if the SSL of the store should be verified.
693 *
694 * @since 1.6.13
695 * @return bool
696 */
697 private function verify_ssl() {
698 return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
699 }
700
701 /**
702 * Gets the unique key (option name) for a plugin.
703 *
704 * @since 1.9.0
705 * @return string
706 */
707 private function get_cache_key() {
708 $string = $this->slug . $this->api_data['license'] . $this->beta;
709
710 return 'edd_sl_' . md5( serialize( $string ) );
711 }
712
713 }