diff --git a/addons/badges/includes/mycred-badge-functions.php b/addons/badges/includes/mycred-badge-functions.php index c5c4eb6..e351efb 100644 --- a/addons/badges/includes/mycred-badge-functions.php +++ b/addons/badges/includes/mycred-badge-functions.php @@ -663,8 +663,10 @@ function mycred_display_users_badges( $user_id = NULL, $width = MYCRED_BADGE_WID else if( $badge->main_image !== false ) $badge_image = $badge->get_image( 'main' ); - if ( !empty( $badge_image ) ) - echo wp_kses_post( apply_filters( 'mycred_the_badge', $badge_image, $badge_id, $badge, $user_id ) ); + if ( !empty( $badge_image ) ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo apply_filters( 'mycred_the_badge', $badge_image, $badge_id, $badge, $user_id ); + } } @@ -960,93 +962,6 @@ function mycred_get_level_image_url( $setup ) { } endif; -/** - * Cretae Evidence page - * @since 2.1 - * @version 1.0 - */ -if ( ! function_exists( 'mycred_get_evidence_page_id' ) ) : - function mycred_get_evidence_page_id() { - - $evidencePageId = 0; - - $badges = mycred_get_addon_settings( 'badges' ); - - //If Open badge enabled - if ( isset( $badges['open_badge'] ) && $badges['open_badge'] == '1' ) { - - $canCreatePage = true; - - $evidence_page_refrence = mycred_get_option( 'open_badge_evidence_page', 0 ); - - if ( ! empty( $badges['open_badge_evidence_page'] ) || ! empty( $evidence_page_refrence ) ) { - - $pageId = intval( $evidence_page_refrence ); - - if ( ! empty( $badges['open_badge_evidence_page'] ) ) { - - $pageId = intval( $badges['open_badge_evidence_page'] ); - - } - - if ( get_post_status( $pageId ) == 'publish' ) { - - $canCreatePage = false; - $evidencePageId = $pageId; - - } - - } - - if ( $canCreatePage ) { - - $postData = array( - 'post_content' => '[' . MYCRED_SLUG . '_badge_evidence]', - 'post_title' => 'Badge Evidence', - 'post_status' => 'publish', - 'post_type' => 'page', - 'comment_status' => 'closed', - 'post_name' => 'Badge Evidence' - ); - - $pageId = wp_insert_post( $postData ); - - $evidencePageId = intval( $pageId ); - - mycred_update_option( 'open_badge_evidence_page', $evidencePageId ); - - mycred_set_badge_evidence_page( $evidencePageId ); - - } - - } - - return $evidencePageId; - - } -endif; - -/** - * Set Evidence page - * @since 2.1 - * @version 1.0 - */ -if ( ! function_exists( 'mycred_set_badge_evidence_page' ) ) : - function mycred_set_badge_evidence_page( $page_id ) { - - $settings = mycred_get_option( 'mycred_pref_core' ); - - if ( isset( $settings[ 'badges' ] ) ) { - - $settings[ 'badges' ][ 'open_badge_evidence_page' ] = intval( $page_id ); - - mycred_update_option( 'mycred_pref_core', $settings ); - - } - - } -endif; - /** * Get badges list * @since 2.1.1 @@ -1224,50 +1139,6 @@ function mycred_badge_show_congratulation_msg( $user_id, $badge, $settings = NUL } endif; -/** - * Returns Badge main image with share icons. - * @since 2.2 - * @version 1.0 - */ -if ( ! function_exists( 'mycred_badge_show_main_image_with_social_icons' ) ) : - function mycred_badge_show_main_image_with_social_icons( $user_id, $badge, $mycred = NULL ) { - - $content = ''; - - $image_url = $badge->get_earned_image( $user_id ); - - if ( ! empty( $image_url ) ) { - - $content .= '
'; - - $content .= 'Badge Image'; - - if ( empty( $mycred ) ) $mycred = mycred(); - - //If user has earned badge, show user sharing badge option - if( - $badge->user_has_badge( $user_id ) && - ! empty( $mycred->core["br_social_share"]["enable_open_badge_ss"] ) - ) { - - $facebook_url = "http://www.facebook.com/sharer.php?u=".get_permalink()."&p[images][0]=$image_url"; - $twitter_url = "https://twitter.com/share?url=".get_permalink().""; - $linkedin_url = "http://www.linkedin.com/shareArticle?url=".get_permalink().""; - $pinterest_url = "https://pinterest.com/pin/create/bookmarklet/?media=$image_url&url=".get_permalink().""; - - $content .= mycred_br_get_social_icons( $facebook_url, $twitter_url, $linkedin_url, $pinterest_url ); - - } - - $content .= '
'; - - } - - return apply_filters( 'mycred_badge_show_main_image_with_social_icons', $content, $badge, $mycred ); - - } -endif; - /** * Returns Badge description. * @since 2.2 diff --git a/addons/badges/includes/mycred-badge-object.php b/addons/badges/includes/mycred-badge-object.php index 3b451c5..01083a5 100644 --- a/addons/badges/includes/mycred-badge-object.php +++ b/addons/badges/includes/mycred-badge-object.php @@ -106,15 +106,13 @@ protected function populate( $object = NULL, $level_id = NULL ) { // Indicate open badge if ( absint( mycred_get_post_meta( $this->post_id, 'open_badge', true ) ) === 1 ) { - - $badge_setting = mycred_get_addon_settings( 'badges' ); + $badge_setting = mycred_get_option( 'mycred_pref_core' ); - if ( isset( $badge_setting['open_badge'] ) && $badge_setting['open_badge'] === 1 ) { + if ( isset( $badge_setting['open_badge'] ) && absint($badge_setting['open_badge']['is_enabled'] ) === 1 ) { $this->open_badge = true; } - } // If we requested a particular level @@ -806,7 +804,7 @@ public function get_earned_image( $user_id ) { if ( ! file_exists( $open_badge_directory . $filename ) ) { $mycred_Open_Badge = new mycred_Open_Badge(); - $mycred_Open_Badge->bake_users_image( $user_id, $badge_id ); + $mycred_Open_Badge->bake_users_image( $user_id, $badge_id, $image_url, $this->title, $this->open_badge ); } diff --git a/addons/badges/includes/mycred-badge-shortcodes.php b/addons/badges/includes/mycred-badge-shortcodes.php index c777d3b..3ee83b9 100644 --- a/addons/badges/includes/mycred-badge-shortcodes.php +++ b/addons/badges/includes/mycred-badge-shortcodes.php @@ -83,7 +83,9 @@ function mycred_render_my_badges( $atts, $content = '' ) { } echo ''; + do_action( 'mycred_add_share_and_embed_button', $badge, $badge_id ); + } echo ''; @@ -247,54 +249,6 @@ function mycred_render_badges_list( $atts = '' ) { Evidence not found'; - - if ( isset( $_GET['uid'] ) && isset( $_GET['bid'] ) ) { - - $user_id = intval( $_GET['uid'] ); - $badge_id = intval( $_GET['bid'] ); - - $user_info = get_userdata( $user_id ); - $badge = mycred_get_badge( $badge_id ); - - if ( $user_info && $badge && $badge->open_badge ) { - - $issued_on = mycred_get_user_meta( $user_id, MYCRED_BADGE_KEY . $badge_id, '_issued_on', true ); - - $content = '
-
- -
-
-

' . $badge->title . '

-
-

Name: '. $user_info->display_name .'

-

Email: ' . $user_info->user_email . '

-

Issued On: ' . date( 'Y-m-d\TH:i:sP', $issued_on ) . '

-

Verified

-
-
-
-
'; - - } - - - } - - return $content; - } -endif; \ No newline at end of file diff --git a/addons/badges/myCRED-addon-badges.php b/addons/badges/myCRED-addon-badges.php index d54b7d5..ee29edd 100644 --- a/addons/badges/myCRED-addon-badges.php +++ b/addons/badges/myCRED-addon-badges.php @@ -31,8 +31,7 @@ require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-badge-functions.php'; require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-badge-shortcodes.php'; require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-badge-object.php'; -require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-badge-secondary.php'; -require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-open-badge.php'; +require_once MYCRED_BADGE_INCLUDES_DIR . 'mycred-badge-secondary.php';; /** * myCRED_buyCRED_Module class @@ -117,8 +116,6 @@ public function module_init() { add_shortcode( MYCRED_SLUG . '_badges_list', 'mycred_render_badges_list' ); - add_shortcode( MYCRED_SLUG . '_badge_evidence', 'mycred_render_badge_evidence' ); - // Insert into bbPress if ( class_exists( 'bbPress' ) ) { @@ -129,11 +126,14 @@ public function module_init() { add_action( 'bbp_theme_after_reply_author_details', array( $this, 'insert_into_bbpress_reply' ) ); } + $this->open_badge = false; - //Load open badge if enabled - if ( $this->badges['open_badge'] ) { - + $setting = mycred_get_option( 'mycred_pref_core' ); + if ( isset( $setting['open_badge'] ) && $setting['open_badge']['is_enabled'] == 1 ) { + + $this->open_badge = $setting['open_badge']['is_enabled']; $this->mycred_open_badge_init(); + add_action( 'mycred_open_badges_html', array( $this, 'mycred_badge_button_html' ), 10 ); } @@ -159,6 +159,19 @@ public function module_init() { } + /** + * Enqueue Front End Script + * @since 1.3 + * @version 1.0 + */ + public function mycred_badge_button_html() { ?> + +
+ +
true, 'has_archive' => false, 'exclude_from_search' => true, - 'publicly_queryable' => false, 'register_meta_box_cb' => array( $this, 'add_metaboxes' ), 'capability_type' => 'post', 'publicly_queryable' => true, @@ -603,7 +615,7 @@ public function adjust_column_headers( $defaults ) { $columns['badge-users'] = __( 'Users', 'mycred' ); $columns['badge-type'] = __( 'Achievements Type', 'mycred' ); - if ( $this->badges['open_badge'] ) + if ( $this->open_badge ) $columns['badge-open-badge'] = __( 'Open Badge', 'mycred' ); // Return @@ -796,7 +808,7 @@ public function add_metaboxes() { 'low' ); - if ( $this->badges['open_badge'] ) + if ( $this->open_badge ) add_meta_box( 'mycred-badge-open-badge', __( 'Open Badge', 'mycred' ), @@ -1078,7 +1090,7 @@ public function metabox_badge_setup( $post ) { $point_types = mycred_get_types( true ); $open_badge = false; - if ( $this->badges['open_badge'] == 1 ) { + if ( $this->open_badge == 1 ) { $open_badge = ( mycred_get_post_meta( $post->ID, 'open_badge', true ) == 1 ) ? true : false; @@ -1581,41 +1593,6 @@ public function after_general_settings( $mycred = NULL ) { -

-
-
-
-
- -
-
-
- -
-
- - $this->field_id( 'open_badge_evidence_page' ), - 'name' => $this->field_name( 'open_badge_evidence_page' ), - 'selected' => $selectedEvidencePage - ); - - wp_dropdown_pages( esc_attr( $args ) ); - ?> -
-
-
-
- -
-
- -
-

@@ -1712,9 +1689,7 @@ public function sanitize_extra_settings( $new_data, $data, $core ) { $new_data['badges']['buddypress'] = ( isset( $data['badges']['buddypress'] ) ) ? sanitize_text_field( $data['badges']['buddypress'] ) : ''; $new_data['badges']['bbpress'] = ( isset( $data['badges']['bbpress'] ) ) ? sanitize_text_field( $data['badges']['bbpress'] ) : ''; - - $new_data['badges']['open_badge'] = ( isset( $data['badges']['open_badge'] ) ) ? intval( $data['badges']['open_badge'] ) : 0; - + //Specific Badge Page Setup @since 2.1 $new_data['badges']['show_level_description'] = ( isset( $data['badges']['show_level_description'] ) ) ? intval( $data['badges']['show_level_description'] ) : 0; $new_data['badges']['show_congo_text'] = ( isset( $data['badges']['show_congo_text'] ) ) ? intval( $data['badges']['show_congo_text'] ) : 0; @@ -1722,8 +1697,6 @@ public function sanitize_extra_settings( $new_data, $data, $core ) { $new_data['badges']['show_level_points'] = ( isset( $data['badges']['show_level_points'] ) ) ? intval( $data['badges']['show_level_points'] ) : 0; $new_data['badges']['show_steps_to_achieve'] = ( isset( $data['badges']['show_steps_to_achieve'] ) ) ? intval( $data['badges']['show_steps_to_achieve'] ) : 0; $new_data['badges']['show_earners'] = ( isset( $data['badges']['show_earners'] ) ) ? intval( $data['badges']['show_earners'] ) : 0; - $new_data['badges']['open_badge_evidence_page'] = ( isset( $data['badges']['open_badge_evidence_page'] ) ) ? intval( $data['badges']['open_badge_evidence_page'] ) : 0; - return $new_data; @@ -2066,11 +2039,25 @@ public function mycred_open_badge_init() { $mycred_Open_Badge = new mycred_Open_Badge(); - add_action( 'mycred_after_badge_assign', array( $mycred_Open_Badge, 'bake_users_image' ), 10, 2 ); + add_action( 'mycred_after_badge_assign', array( $this, 'after_badge_assign' ), 10, 2 ); add_action( 'rest_api_init', array( $mycred_Open_Badge, 'register_open_badge_routes' ) ); } + /** + * Init Open Badge + * @since 2.1 + * @version 1.0 + */ + public function after_badge_assign( $user_id, $badge_id ) { + + $mycred_Open_Badge = new mycred_Open_Badge(); + $badge = mycred_get_badge( $badge_id ); + + $mycred_Open_Badge->bake_users_image( $user_id, $badge_id, $badge->main_image_url, $badge->title, $this->open_badge ); + + } + /** * Loads meta in header for Social Sharing * @since 2.2 @@ -2130,7 +2117,7 @@ public function mycred_badge_page_template( $content ) { $content .= '
'; if( $badge->layout != 'mycred_layout_bottom' ) - $content .= mycred_badge_show_main_image_with_social_icons( $user_id, $badge, $mycred ); + $content .= mycred_badge_show_main_image_with_social_icons( $badge->get_earned_image( $user_id), $badge->user_has_badge( $user_id ) ); $content .= '
'; @@ -2141,7 +2128,7 @@ public function mycred_badge_page_template( $content ) { $content .= '
'; if( $badge->layout == 'mycred_layout_bottom' ) - $content .= mycred_badge_show_main_image_with_social_icons( $user_id, $badge, $mycred ); + $content .= mycred_badge_show_main_image_with_social_icons( $badge->get_earned_image( $user_id), $badge->user_has_badge( $user_id ) ); $content .= '
'; diff --git a/addons/banking/services/mycred-service-central.php b/addons/banking/services/mycred-service-central.php index 93b4120..54d7cdd 100644 --- a/addons/banking/services/mycred-service-central.php +++ b/addons/banking/services/mycred-service-central.php @@ -108,7 +108,10 @@ public function preferences() { if ( ! empty( $this->prefs['bank_id'] ) ) $user = get_userdata( $this->prefs['bank_id'] ); - if( ! empty( mycred_get_option('mycred_pref_bank')['active'] ) && in_array( 'central', mycred_get_option('mycred_pref_bank')['active'] ) ) { + $settings = mycred_get_banking_addon_settings( NULL, $this->core->cred_id ); + + if ( ! empty( $settings['active'] ) && in_array( 'central', $settings['active'] ) ) { + ?>
diff --git a/addons/banking/services/mycred-service-schedule-deposit.php b/addons/banking/services/mycred-service-schedule-deposit.php index b65776e..70aaf12 100644 --- a/addons/banking/services/mycred-service-schedule-deposit.php +++ b/addons/banking/services/mycred-service-schedule-deposit.php @@ -7,7 +7,7 @@ * @version 1.0.1 */ if ( ! class_exists( 'myCRED_Banking_Service_Schedule_Deposit' ) ) : - class myCRED_Banking_Service_Schedule_Deposit extends myCRED_Service { + class myCRED_Banking_Service_Schedule_Deposit extends myCRED_Service { /** * Construct @@ -44,7 +44,7 @@ public function run() { } - add_action( 'mycred_schedule_deposit_event', array( $this, 'scheduled_event' ) ); + add_action( 'mycred_schedule_deposit_event_' . $this->core->cred_id, array( $this, 'scheduled_event' ) ); add_filter( 'mycred_check_schedule_deposite_entry', array( $this, 'mycred_check_schedule_deposite' ), 10, 4 ); add_action( 'mycred_banking_settings_save', array( $this, 'mycred_save_banking_setting' ), 10, 2 ); @@ -63,24 +63,18 @@ public function mycred_check_schedule_deposite( $con, $reply, $request, $mycred // when will cron work and to save all setting of central deposit schedule public function mycred_save_banking_setting( $post, $obj ) { - if ( in_array( 'central', (array) $obj->active ) && ! in_array( 'central', $post['active'] ) ) { + if ( ! empty( $post['active'] ) && in_array( 'schedule_deposit', (array) $post['active'] ) ) { - $post['active'] = array(); - - } - - if( isset( $post['active'][1] ) && $post['active'][1] == 'schedule_deposit' ){ - - if( ! wp_next_scheduled( 'mycred_schedule_deposit_event' ) ) { + if( ! wp_next_scheduled( 'mycred_schedule_deposit_event_' . $obj->core->cred_id ) ) { - wp_schedule_event( time(), 'daily', 'mycred_schedule_deposit_event' ); + wp_schedule_event( time(), 'daily', 'mycred_schedule_deposit_event_' . $obj->core->cred_id ); } } else { - wp_clear_scheduled_hook( 'mycred_schedule_deposit_event' ); + wp_clear_scheduled_hook( 'mycred_schedule_deposit_event_' . $obj->core->cred_id ); } @@ -139,9 +133,11 @@ public function scheduled_event() { public function preferences() { $prefs = $this->prefs; - - if( ! empty( mycred_get_option('mycred_pref_bank')['active'] ) && in_array( 'schedule_deposit', mycred_get_option('mycred_pref_bank')['active'] ) ) { - ?> + + $settings = mycred_get_banking_addon_settings( NULL, $this->core->cred_id ); + + if ( ! empty( $settings['active'] ) && in_array( 'schedule_deposit', $settings['active'] ) ) { +?>
@@ -249,13 +245,15 @@ public function deactivate() { wp_clear_scheduled_hook( 'mycred_schedule_deposit_event' ); - - } public function mycred_notice_banking_email_check( $emailnotice, $request, $mycred ) { + + $setting = mycred_get_option('mycred_pref_bank'); + + if ( empty( $setting['service_prefs']['central']['bank_id'] ) ) return $emailnotice; - $user_bank_id = mycred_get_option('mycred_pref_bank')['service_prefs']['central']['bank_id']; + $user_bank_id = $setting['service_prefs']['central']['bank_id']; $point_type = $mycred->get_point_type_key(); $min_balance_emails = mycred_get_event_emails( $point_type, 'generic', 'central_min_balance' ); $no_balance_emails = mycred_get_event_emails( $point_type, 'generic', 'central_no_balance' ); @@ -315,13 +313,10 @@ public function mycred_notice_banking_email_check( $emailnotice, $request, $mycr } - if ( empty( $emails ) ) return; - return $emailnotice; + } } -endif; - - +endif; \ No newline at end of file diff --git a/addons/buy-creds/abstracts/mycred-abstract-payment-gateway.php b/addons/buy-creds/abstracts/mycred-abstract-payment-gateway.php index 4c51c4a..3a01923 100644 --- a/addons/buy-creds/abstracts/mycred-abstract-payment-gateway.php +++ b/addons/buy-creds/abstracts/mycred-abstract-payment-gateway.php @@ -496,7 +496,7 @@ public function checkout_order() { $cost_label = apply_filters( 'mycred_buycred_checkout_order', __('Cost', 'mycred'), $this ); if ( $this->gifting ) - $table_rows[] = '' . esc_js( esc_attr($cost_label ) ) . ': ' . esc_html( get_userdata( $this->recipient_id )->display_name ) . ''; + $table_rows[] = 'Recipient' . esc_html( get_userdata( $this->recipient_id )->display_name ) . ''; $table_rows[] = '' . esc_js( esc_attr( __( 'Cost', 'mycred' ) ) ) . '' . sprintf( '%s %s', apply_filters( 'mycred_buycred_display_user_amount', $this->cost ), $this->prefs['currency'] ) . ''; @@ -507,8 +507,8 @@ public function checkout_order() { - @@ -888,12 +888,6 @@ public function get_point_type() { */ public function get_cost( $amount = 0, $point_type = MYCRED_DEFAULT_TYPE_KEY, $raw = false, $custom_rate = 0 ) { - if( ! empty( $_REQUEST['er_random'] ) ) { - - $custom_rate = mycred_decode_values( sanitize_text_field( wp_unslash( $_REQUEST['er_random'] ) ) ); - - } - $setup = mycred_get_buycred_sale_setup( $point_type ); // Apply minimum @@ -903,12 +897,15 @@ public function get_cost( $amount = 0, $point_type = MYCRED_DEFAULT_TYPE_KEY, $r // Calculate cost here so we can use any exchange rate if ( array_key_exists( $point_type, $this->prefs['exchange'] ) ) { - // Check for user override $override = mycred_get_user_meta( $this->current_user_id, 'mycred_buycred_rates_' . $point_type, '', true ); - if ( isset( $override[ $this->id ] ) && $override[ $this->id ] != '' ) - $rate = $override[ $this->id ]; - else if( $custom_rate !=0 ) + + if ( $custom_rate == 0 && isset( $_REQUEST['er_random'] ) ) + $custom_rate = mycred_decode_values( sanitize_text_field( wp_unslash( $_REQUEST['er_random'] ) ) ); + + if( $custom_rate != 0 ) $rate = $custom_rate; + else if( isset( $override[ $this->id ] ) && $override[ $this->id ] != '' ) + $rate = $override[ $this->id ]; else $rate = $this->prefs['exchange'][ $point_type ]; @@ -917,7 +914,7 @@ public function get_cost( $amount = 0, $point_type = MYCRED_DEFAULT_TYPE_KEY, $r else $rate = (int) $rate; - $cost = $amount * $rate; + $cost = $amount * $rate; } else diff --git a/addons/buy-creds/assets/js/checkout.js b/addons/buy-creds/assets/js/checkout.js index aedf430..2b96156 100644 --- a/addons/buy-creds/assets/js/checkout.js +++ b/addons/buy-creds/assets/js/checkout.js @@ -25,6 +25,7 @@ jQuery(function($){ buyCREDform.slideUp(function(){ buyCREDform.empty().append( response ).slideDown(); }); + buyCREDcancel.addClass( 'on' ); } else { buyCREDform.slideUp(function(){ diff --git a/addons/buy-creds/gateways/bitpay.php b/addons/buy-creds/gateways/bitpay.php index 310b4b1..c1bcaee 100644 --- a/addons/buy-creds/gateways/bitpay.php +++ b/addons/buy-creds/gateways/bitpay.php @@ -148,49 +148,49 @@ public function prep_sale( $new_transaction = false ) { try { // Bitpay url - $host = 'bitpay.com'; + $host = 'bitpay.com'; + if ( $this->sandbox_mode ) $host = 'test.bitpay.com'; - $request_body = - json_encode( - array( - 'currency' => $this->currency, - 'price' => $this->cost, - 'orderId' => $this->transaction_id, - 'notificationURL' => $this->callback_url(), - 'redirectURL' => $this->get_thankyou(), - 'fullNotifications' => ( ( $this->prefs['notifications'] ) ? true : false ), - 'transactionSpeed' => $this->prefs['speed'], - 'description' => $item_name, - 'buyer' => array( - 'email' => $user->user_email + $request_body = json_encode( + array( + 'currency' => $this->currency, + 'price' => $this->cost, + 'orderId' => $this->transaction_id, + 'notificationURL' => $this->callback_url(), + 'redirectURL' => $this->get_thankyou(), + 'fullNotifications' => ( ( $this->prefs['notifications'] ) ? true : false ), + 'transactionSpeed' => $this->prefs['speed'], + 'description' => $item_name, + 'buyer' => array( + 'email' => $user->user_email + ), + 'token' => $api_token + ) + ); + + $create_invoice = wp_remote_post( 'https://'.$host.'/invoices', + array( + 'method' => 'POST', + 'headers' => + array( + 'X-Accept-Version' => '2.0.0', + 'Content-Type' => 'application/json' ), - 'token' => $api_token - ), - ); - - $create_invoice = - wp_remote_post( 'https://'.$host.'/invoices', - array( - 'method' => 'POST', - 'headers' => - array( - 'X-Accept-Version' => '2.0.0', - 'Content-Type' => 'application/json' - ), - 'body' => $request_body - ) - ); - - } catch ( \Exception $e ) { + 'body' => $request_body + ) + ); + + } + catch ( \Exception $e ) { $this->errors[] = $e->getMessage(); } if ( empty( $this->errors ) ) { - + $this->redirect_to = json_decode( $create_invoice['body'] )->data->url; } @@ -224,13 +224,13 @@ public function ajax_buy() { */ public function checkout_page_body() { - echo $this->checkout_header(); - echo $this->checkout_logo( false ); + echo wp_kses_post( $this->checkout_header() ); + echo wp_kses_post( $this->checkout_logo( false ) ); - echo $this->checkout_order(); - echo $this->checkout_cancel(); + echo wp_kses_post( $this->checkout_order() ); + echo wp_kses_post( $this->checkout_cancel() ); - echo $this->checkout_footer(); + echo wp_kses_post( $this->checkout_footer() ); } @@ -245,10 +245,10 @@ function preferences() { ?>
-

+

- +
@@ -275,18 +275,18 @@ function preferences() {
- - + +
- - + +
- - __( 'Low', 'mycred' ) ); foreach ( $options as $value => $label ) { - echo ''; + echo '>' . esc_html( $label ) . ''; } ?> @@ -307,8 +307,8 @@ function preferences() {
- - __( 'Yes', 'mycred' ) ); foreach ( $options as $value => $label ) { - echo ''; + echo '>' . esc_html( $label ) . ''; } ?> @@ -329,14 +329,14 @@ function preferences() {
-

+

- - + +
- + exchange_rate_setup(); ?> diff --git a/addons/buy-creds/includes/buycred-reward-hook.php b/addons/buy-creds/includes/buycred-reward-hook.php index 39a2f42..2168a5d 100644 --- a/addons/buy-creds/includes/buycred-reward-hook.php +++ b/addons/buy-creds/includes/buycred-reward-hook.php @@ -173,7 +173,7 @@ public function buycred_reward_setting( $data ){
- available_template_tags( array( 'general' ) ) ); ?> + available_template_tags( array( 'general' ) ) ); ?>
@@ -186,7 +186,7 @@ public function buycred_reward_setting( $data ){
- +
diff --git a/addons/cash-creds/assets/css/admin-style.css b/addons/cash-creds/assets/css/admin-style.css index 5b2498d..d294516 100644 --- a/addons/cash-creds/assets/css/admin-style.css +++ b/addons/cash-creds/assets/css/admin-style.css @@ -64,6 +64,19 @@ #payment_gateway_detail input.form-control { width: 100%; } + +.myCred_cashcred_payment_gateway_details { + margin-top: 2px; + margin-bottom: 20px; + width: 100%; + resize: none; +} +.cashcred_radio { + width: auto !important; +} +.cashcred_textarea { + resize: none; +} #payment_gateway_detail .inside{ padding: 4px;} .cashcred_panel { background-color: #e6e6e6; diff --git a/addons/cash-creds/gateways/bank-transfer.php b/addons/cash-creds/gateways/bank-transfer.php index d7267b8..fc09f25 100644 --- a/addons/cash-creds/gateways/bank-transfer.php +++ b/addons/cash-creds/gateways/bank-transfer.php @@ -222,42 +222,49 @@ public function form_fields() { $gateway_fields = array( 'ac_name' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Account name', 'classes' => 'form-control', 'placeholder' => 'Account name', ), 'ac_number' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Account number', 'classes' => 'form-control', 'placeholder' => 'Account number', ), 'ac_code' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Sort code', 'classes' => 'form-control', 'placeholder' => 'Sort code', ), 'ba_name' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Bank name', 'classes' => 'form-control', 'placeholder' => 'Bank name', ), 'ro_number' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Routing number', 'classes' => 'form-control', 'placeholder' => 'Routing number', ), 'ib_name' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'IBAN', 'classes' => 'form-control', 'placeholder' => 'IBAN', ), 'sw_code' => array( + 'form' => 'input', 'type' => 'text', 'lable' => 'Swift code', 'classes' => 'form-control', diff --git a/addons/cash-creds/includes/cashcred-functions.php b/addons/cash-creds/includes/cashcred-functions.php index 73ad68f..965120d 100644 --- a/addons/cash-creds/includes/cashcred-functions.php +++ b/addons/cash-creds/includes/cashcred-functions.php @@ -438,11 +438,46 @@ public function field_name( $field_name ) { public function generate_form() { - foreach ( $this->gateway_fields as $gateway_field_id => $gateway_field_data ): ?> - -
+ foreach ( $this->gateway_fields as $gateway_field_id => $gateway_field_data ): + ?> +
- + + + + + $value ) { + ?> +
+
+ + +
- - post_date = date('F d, Y, h:i A'); ?> @@ -202,7 +200,7 @@ function mycred_render_cashcred( $atts = array(), $content = '' ) { $amount = ! empty( $amount ) ? floatval( $amount ) : 0; ?> - +

post_date = date('F d, Y, h:i A'); ?> @@ -371,8 +368,6 @@ function mycred_render_cashcred( $atts = array(), $content = '' ) { post_date = date('F d, Y, h:i A'); - ?> diff --git a/addons/cash-creds/modules/cashcred-module-withdrawal.php b/addons/cash-creds/modules/cashcred-module-withdrawal.php index 9bdc7ef..770415d 100644 --- a/addons/cash-creds/modules/cashcred-module-withdrawal.php +++ b/addons/cash-creds/modules/cashcred-module-withdrawal.php @@ -82,7 +82,7 @@ public function module_admin_init() { public function cashcred_redirect_addnew_button() { - if( $_GET["post_type"] == "cashcred_withdrawal" ) + if( ! empty( $_GET["post_type"] ) && $_GET["post_type"] == "cashcred_withdrawal" ) wp_redirect("edit.php?post_type=cashcred_withdrawal"); } diff --git a/addons/gateway/event-booking/mycred-eventsmanager.php b/addons/gateway/event-booking/mycred-eventsmanager.php index 1dd48f0..5f7a678 100644 --- a/addons/gateway/event-booking/mycred-eventsmanager.php +++ b/addons/gateway/event-booking/mycred-eventsmanager.php @@ -1,6 +1,7 @@ __( 'Pay', 'mycred' ) ), 'messages' => array( + 'text' => __( '', 'mycred' ), 'success' => __( 'Thank you for your payment!', 'mycred' ), - 'error' => __( "I'm sorry but you can not pay for these tickets using %_plural%", 'mycred' ) + 'error' => __( "I'm sorry but you can not pay for these tickets using %_plural%", 'mycred' ), + 'url' => __( '', 'mycred' ) ) ); + // Settings $settings = get_option( 'mycred_eventsmanager_gateway_prefs', $defaults ); + $this->prefs = wp_parse_args( $settings, $defaults ); $this->mycred_type = $this->prefs['type']; @@ -53,6 +58,8 @@ function __construct() { // Apply Whitelabeling $this->label = mycred_label(); + + } /** @@ -62,34 +69,86 @@ function __construct() { */ public function load() { + add_action('em_my_bookings_booking_loop', array($this,'payment_box'),999); + add_action('em_template_my_bookings_footer', array($this,'insert_scripting'),99); + + + add_filter('em_booking_get_status', array( $this,'em_booking_get_status'),40,2); + add_filter('em_my_bookings_booking_actions', array($this,'add_pay_button'),99,2); + + // Ajax Payments + add_action('wp_ajax_mycred-pay-em-booking',array($this,'process_payment'),999); + // Settings - add_action( 'em_options_page_footer_bookings', array( $this, 'settings_page' ) ); - add_action( 'em_options_save', array( $this, 'save_settings' ) ); + add_action('em_options_page_footer_bookings', array($this, 'settings_page')); + add_action('em_options_save', array( $this, 'save_settings')); // In case gateway has not yet been enabled bail here. - if ( ! $this->use_gateway() ) return; + if ( ! $this->use_gateway() ) return; // Currency - add_filter( 'em_get_currencies', array( $this, 'add_currency' ) ); + add_filter('em_get_currencies', array( $this, 'add_currency' )); if ( $this->single_currency() ) - add_filter( 'em_get_currency_formatted', array( $this, 'format_price' ), 10, 4 ); + add_filter('em_get_currency_formatted', array($this,'format_price'),10,4); // Adjust Ticket Columns - add_filter( 'em_booking_form_tickets_cols', array( $this, 'ticket_columns' ), 10, 2 ); - add_action( 'em_booking_form_tickets_col_mycred', array( $this, 'ticket_col' ), 10, 2 ); + add_filter('em_booking_form_tickets_cols', array($this,'ticket_columns'),10,2); + add_action('em_booking_form_tickets_col_mycred', array($this,'ticket_col'),10,2); - // Add Pay Button - add_filter( 'em_my_bookings_booking_actions', array( $this, 'add_pay_button' ), 1, 2 ); - add_action( 'em_my_bookings_booking_loop', array( $this, 'payment_box' ) ); - add_action( 'em_template_my_bookings_footer', array( $this, 'insert_scripting' ) ); + + if ( $this->prefs['refund'] != 0 ) { + add_filter('em_booking_set_status', array($this,'refunds'),10,2); + } - // Ajax Payments - add_action( 'wp_ajax_mycred-pay-em-booking', array( $this, 'process_payment' ) ); - if ( $this->prefs['refund'] != 0 ) - add_filter( 'em_booking_set_status', array( $this, 'refunds' ), 10, 2 ); + + add_filter('mycred_booking_failed_text', array($this,'mycred_booking_failed_text'),102); + add_filter('mycred_booking_msg_text', array($this,'mycred_booking_msg_text'),102); } + + public function mycred_booking_msg_text($text) { + + global $EM_Ticket, $EM_Event; + $EM_Booking = $EM_Event->get_bookings()->has_booking(); + $balance = $this->core->get_users_balance($EM_Booking->person_id,$this->mycred_type); + + if(!$this->has_paid( $EM_Booking ) && $balance < $EM_Booking->booking_price) { + + return ' You cannot attend this event. '; + } + else { + + return ' ' . 'You already attending to this event.' . ' '; + } + + } + + + + public function mycred_booking_failed_text($text) { + + global $EM_Booking; + $link_tag_url = '' . $this->prefs['messages']['text'] . ''; + $message = str_replace( '%url%', $link_tag_url, $this->prefs['messages']['error'] ); + $balance = $this->core->get_users_balance( $EM_Booking->person->ID, $this->mycred_type ); + $price = $this->core->number( $EM_Booking->booking_price ); + + if ($balance < $price) { + + return $message; + } + + else { + return $text; + } + + return $text; + + } + + + /** * Add Currency * Adds "Points" as a form of currency @@ -140,7 +199,8 @@ public function format_price( $formatted_price, $price, $currency, $format ) { */ public function use_gateway() { - if ( $this->prefs['setup'] == 'off' ) return false; + + if($this->prefs['setup'] == 'off') return false; return true; @@ -168,15 +228,17 @@ public function single_currency() { public function can_pay( $EM_Booking ) { $EM_Event = $EM_Booking->get_event(); + $users_balance = $this->core->get_users_balance( $EM_Booking->person->ID, $this->mycred_type ); // You cant pay for free events - if ( $EM_Event->is_free() ) return false; + if ( $EM_Event->is_free() ) return false; // Only pending events can be paid for if ( $EM_Event->get_bookings()->has_open_time() ) { $balance = $this->core->get_users_balance( $EM_Booking->person->ID, $this->mycred_type ); - if ( $balance <= 0 ) return false; + + if ( $balance < 0 ) return false; $price = $this->core->number( $EM_Booking->booking_price ); if ( $price == 0 ) return true; @@ -184,8 +246,11 @@ public function can_pay( $EM_Booking ) { if ( ! $this->single_currency() ) { $exchange_rate = $this->prefs['rate']; $price = $this->core->number( $exchange_rate * $price ); + + } + if ( $balance - $price < 0 ) return false; return true; @@ -196,6 +261,22 @@ public function can_pay( $EM_Booking ) { } + public function em_booking_get_status($status,$data) { + + global $mycred; + $user_selected_point_type = get_option('mycred_eventsmanager_gateway_prefs' )['type']; + $users_balance = mycred_get_users_balance( $data->person_id, $user_selected_point_type ); + + if(get_option('dbem_bookings_approval') == 0 && ! $this->has_paid( $data )) { + + $status = 'Pending'; + + } + + return $status; + + } + /** * Has Paid * Checks if the user has paid for booking @@ -204,6 +285,7 @@ public function can_pay( $EM_Booking ) { */ public function has_paid( $EM_Booking ) { + if ( $this->core->has_entry( 'ticket_purchase', $EM_Booking->event->post_id, $EM_Booking->person->ID, array( 'ref_type' => 'post', 'bid' => (int) $EM_Booking->booking_id ), $this->mycred_type ) ) return true; return false; @@ -226,14 +308,18 @@ public function process_payment() { // Get Booking $booking_id = absint( $_POST['booking_id'] ); $booking = em_get_booking( $booking_id ); - + // User if ( $this->core->exclude_user( $booking->person->ID ) ) die( 'ERROR_2' ); + // User can not pay for this if ( ! $this->can_pay( $booking ) ) { - $message = $this->prefs['messages']['error']; + $link_tag_url = '' . $this->prefs['messages']['text'] . ''; + $message = str_replace( '%url%', $link_tag_url, $this->prefs['messages']['error'] ); + + // $message = $status = 'ERROR'; // Let others play @@ -263,8 +349,10 @@ public function process_payment() { ); // Update Booking if approval is required (with option to disable this feature) - if ( get_option( 'dbem_bookings_approval' ) == 1 && apply_filters( 'mycred_em_approve_on_pay', true, $booking, $this ) ) - $booking->approve(); + if ( get_option( 'dbem_bookings_approval' ) == 1 || get_option( 'dbem_bookings_approval' ) == 0 && apply_filters( 'mycred_em_approve_on_pay', true, $booking, $this ) ) { + $booking->approve() ; + + } $message = $this->prefs['messages']['success']; $status = 'OK'; @@ -410,10 +498,23 @@ public function ticket_col( $EM_Ticket, $EM_Event ) { */ public function add_pay_button( $cancel_link = '', $EM_Booking ) { + + ?> + + + + can_pay( $EM_Booking ) && ! $this->has_paid( $EM_Booking ) ) { + if ( ! $this->has_paid( $EM_Booking ) ) { if ( ! empty( $cancel_link ) ) $cancel_link .= ' • '; @@ -435,13 +536,16 @@ public function add_pay_button( $cancel_link = '', $EM_Booking ) { */ public function payment_box( $EM_Booking ) { + global $mycred_em_pay; + if ( $mycred_em_pay && is_object( $EM_Booking ) ) { $balance = $this->core->get_users_balance( $EM_Booking->person->ID, $this->mycred_type ); - if ( $balance <= 0 ) return; + if ( $balance < 0 ) return; + $price = $EM_Booking->booking_price; if ( $price == 0 ) return; @@ -451,8 +555,8 @@ public function payment_box( $EM_Booking ) { $price = $this->core->number( $exchange_rate * $price ); } - if ( $balance-$price < 0 ) return; - + + ?> - + + + + + + + $price): ?> + - + +
' . esc_js( esc_attr($item_label ) ) . ' - ' . esc_js( esc_attr($amount_label ) ) . ' + ' . esc_js( esc_attr( $item_label ) ) . ' + ' . esc_js( esc_attr( $amount_label ) ) . '
post_name ); ?> ID,'points',true) );?>
post_name ); ?>
post_name ); ?>
core->format_creds( $price-$balance ) ); ?>
core->format_creds( $balance-$price ) ); ?>core->format_creds($balance-$price)); ?>
@@ -481,15 +593,11 @@ public function payment_box( $EM_Booking ) {

- - prefs['setup'] == 'multi' ) + global $allowedtags; + + $url = str_replace( '%url%',$this->prefs['messages']['error'], $this->prefs['messages']['url'] ); + + if ( $this->prefs['setup'] == 'multi' || $this->prefs['setup'] == 'single' ) $box = 'display: block;'; else $box = 'display: none;'; @@ -691,17 +805,32 @@ public function settings_page() { -
+

core->available_template_tags( array( 'general' ) ) ); ?>
+ + + +
+ + + + + + +
+ + + - - is_plugin() ) { - if ( $this->_is_network_active ) { + if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) { add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 ); + } else { + add_action( 'wp_initialize_site', array( $this, '_after_wp_initialize_site_callback' ), 11, 2 ); } register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) ); @@ -1652,7 +1471,12 @@ private function register_constructor_hooks() { add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) ); add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) ); add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) ); - add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 ); + + if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) { + add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 ); + } else { + add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) ); + } add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) ); add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) ); @@ -1677,6 +1501,7 @@ private function register_constructor_hooks() { add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) ); add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) ); add_action( 'admin_init', array( &$this, '_add_user_change_option' ) ); + add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) ); $this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) ); $this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) ); @@ -1711,13 +1536,17 @@ private function register_constructor_hooks() { add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) ); /** - * Handle request to reset anonymous mode for `get_reconnect_url()`. + * Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 */ - if ( fs_request_is_action( 'reset_anonymous_mode' ) && - $this->get_unique_affix() === fs_request_get( 'fs_unique_affix' ) + if ( + ( + fs_request_is_action( 'reset_anonymous_mode' ) || + fs_request_is_action( 'reset_pending_activation_mode' ) + ) && + $this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' ) ) { add_action( 'admin_init', array( &$this, 'connect_again' ) ); } @@ -2048,7 +1877,13 @@ function _hook_action_links_and_register_account_hooks() { return; } - $this->_add_tracking_links(); + if ( + ( self::is_plugins_page() && $this->is_plugin() ) || + ( self::is_themes_page() && $this->is_theme() ) || + fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) + ) { + $this->_add_tracking_links(); + } if ( self::is_plugins_page() && $this->is_plugin() ) { $this->hook_plugin_action_links(); @@ -2094,20 +1929,27 @@ private function _register_account_hooks() { /** * Leverage backtrace to find caller plugin file path. * - * @author Vova Feldman (@svovaf) - * @since 1.0.6 - * - * @param bool $is_init Is initiation sequence. + * @param bool $is_init Is initiation sequence. + * @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path. * * @return string + * @since 1.0.6 + * + * @author Vova Feldman (@svovaf) */ - private function _find_caller_plugin_file( $is_init = false ) { + private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) { // Try to load the cached value of the file path. if ( isset( $this->_storage->plugin_main_file ) ) { $plugin_main_file = $this->_storage->plugin_main_file; if ( ! empty( $plugin_main_file->path ) ) { $absolute_path = $this->get_absolute_path( $plugin_main_file->path ); if ( file_exists( $absolute_path ) ) { + if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) { + // Update cached path if not matching the actual path. + $plugin_main_file->path = $main_file; + $this->_storage->plugin_main_file = $plugin_main_file; + } + return $absolute_path; } } @@ -2148,12 +1990,11 @@ private function _find_caller_plugin_file( $is_init = false ) { * Only the original instantiator that calls dynamic_init can modify the module's path. */ // Find caller module. - $id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() ); $this->_storage->plugin_main_file = (object) array( - 'path' => $id_slug_type_path_map[ $this->_module_id ]['path'], + 'path' => $main_file, ); - return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] ); + return $this->get_absolute_path( $main_file ); } /** @@ -2215,6 +2056,8 @@ private function get_module_root_dir_path( $module_type = false ) { * @param number $module_id * @param string $slug * + * @return string Since 2.5.0 return the module's main file path. + * * @since 1.2.2 */ private function store_id_slug_type_path_map( $module_id, $slug ) { @@ -2236,20 +2079,52 @@ private function store_id_slug_type_path_map( $module_id, $slug ) { $store_option = true; } - if ( empty( $id_slug_type_path_map[ $module_id ]['path'] ) || - /** - * This verification is for cases when suddenly the same module - * is installed but with a different folder name. - * - * @author Vova Feldman (@svovaf) - * @since 1.2.3 - */ - ! file_exists( $this->get_absolute_path( - $id_slug_type_path_map[ $module_id ]['path'], - $id_slug_type_path_map[ $module_id ]['type'] - ) ) - ) { - $caller_main_file_and_type = $this->get_caller_main_file_and_type(); + $find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] ); + + if ( ! $find_caller ) { + /** + * This verification is for cases when suddenly the same module + * is installed but with a different folder name. + * + * @author Vova Feldman (@svovaf) + * @since 1.2.3 + */ + $find_caller = ! file_exists( $this->get_absolute_path( + $id_slug_type_path_map[ $module_id ]['path'], + $id_slug_type_path_map[ $module_id ]['type'] + ) ); + } + + foreach ( $id_slug_type_path_map as $id => $data ) { + if ( empty( $id ) ) { + // Remove maps with empty module ID. + unset( $id_slug_type_path_map[ $id ] ); + $store_option = true; + continue; + } + + /** + * If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths. + * + * @author Vova Feldman (@svovaf) + * @since 2.5.0 + */ + if ( ! $find_caller ) { + if ( $id == $module_id ) { + continue; + } + + if ( + isset( $data['path'] ) && + $data['path'] === $id_slug_type_path_map[ $module_id ]['path'] + ) { + $find_caller = true; + } + } + } + + if ( $find_caller ) { + $caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id ); $id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type; $id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path; @@ -2260,6 +2135,8 @@ private function store_id_slug_type_path_map( $module_id, $slug ) { if ( $store_option ) { self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true ); } + + return $id_slug_type_path_map[ $module_id ]['path']; } /** @@ -2273,8 +2150,10 @@ private function store_id_slug_type_path_map( $module_id, $slug ) { * add-ons are relying on loading the SDK from the parent module, and also allows themes including the * SDK an internal file instead of directly from functions.php. * @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic. + * + * @param number $module_id @since 2.5.0 */ - private function get_caller_main_file_and_type() { + private function get_caller_main_file_and_type( $module_id ) { self::require_plugin_essentials(); $all_plugins = fs_get_plugins( true ); @@ -2413,10 +2292,12 @@ private function get_caller_main_file_and_type() { } } - return (object) array( + $caller_main_file_and_type = (object) array( 'module_type' => $module_type, 'path' => $caller_file_candidate ); + + return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type ); } #---------------------------------------------------------------------------------- @@ -2433,6 +2314,13 @@ private function get_caller_main_file_and_type() { * @since 1.1.2 */ function _add_deactivation_feedback_dialog_box() { + if ( + $this->is_clone() || + ( is_object( $this->_site ) && ! $this->is_registered() ) + ) { + return; + } + $subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ? $this->_get_subscription_cancellation_dialog_box_template_params() : array(); @@ -2440,7 +2328,7 @@ function _add_deactivation_feedback_dialog_box() { /** * @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter. */ - $show_deactivation_feedback_form = true; + $show_deactivation_feedback_form = ! self::is_deactivation_snoozed(); if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) { $show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true ); } else if ( $this->is_addon() ) { @@ -2545,7 +2433,7 @@ function _get_uninstall_reasons( $user_type = 'long-term' ) { $reason_temporary_deactivation = array( 'id' => self::REASON_TEMPORARY_DEACTIVATION, 'text' => sprintf( - $this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ), + $this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ), strtolower( $this->is_plugin() ? $this->get_text_inline( 'Deactivation', 'deactivation' ) : $this->get_text_inline( 'Theme Switch', 'theme-switch' ) @@ -2710,6 +2598,14 @@ function _submit_uninstall_reason_action() { $this->_storage->store( 'uninstall_reason', $reason ); + if ( self::REASON_TEMPORARY_DEACTIVATION == $reason->id ) { + $snooze_period = fs_request_get( 'snooze_period' ); + + if ( is_numeric( $snooze_period ) && 0 < $snooze_period ) { + self::snooze_deactivation_form( (int) $snooze_period ); + } + } + /** * If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do * not support uninstall hook. @@ -2731,6 +2627,73 @@ function _submit_uninstall_reason_action() { exit; } + #-------------------------------------------------------------------------------- + #region Deactivation Feedback Snoozing + #-------------------------------------------------------------------------------- + + /** + * @author Vova Feldman (@svovaf) + * @since 2.4.3 + * + * @param int $period + * + * @return bool True if the value was set, false otherwise. + */ + private static function snooze_deactivation_form( $period ) { + return ( 0 < $period && self::reset_deactivation_snoozing( $period ) ); + } + + /** + * Check if deactivation feedback form is snoozed. + * + * @author Vova Feldman (@svovaf) + * @since 2.4.3 + * + * @return bool + */ + static function is_deactivation_snoozed() { + $is_snoozed = ( ! is_multisite() || fs_is_network_admin() ) ? + get_transient( 'fs_snooze_period' ) : + get_site_transient( 'fs_snooze_period' ); + + + return ( 'true' === $is_snoozed ); + } + + /** + * Reset deactivation snoozing. When `$period` is `0` will stop deactivation snoozing by deleting the transients. Otherwise, will set the transients for the selected period. + * + * @param int $period Period in seconds. + * + * @author Vova Feldman (@svovaf) + * @since 2.4.3 + */ + private static function reset_deactivation_snoozing( $period = 0 ) { + $value = ( 0 === $period ) ? null : 'true'; + + if ( ! is_multisite() || fs_is_network_admin() ) { + return set_transient( 'fs_snooze_period', $value, $period ); + } else { + return set_site_transient( 'fs_snooze_period', $value, $period ); + } + } + + /** + * The deactivation snooze expiration UNIX timestamp (in sec). + * + * @author Vova Feldman (@svovaf) + * @since 2.4.3 + * + * @return int + */ + static function deactivation_snooze_expires_at() { + return ( ! is_multisite() || fs_is_network_admin() ) ? + (int) get_option( '_transient_timeout_fs_snooze_period' ) : + (int) get_site_option( '_site_transient_timeout_fs_snooze_period' ); + } + + #endregion + /** * @author Leo Fajardo (@leorw) * @since 2.1.4 @@ -2927,6 +2890,13 @@ function get_addon_instance( $id_or_slug ) { return self::instance( $addon_id ); } + /** + * @return Freemius[] + */ + static function _get_all_instances() { + return self::$_instances; + } + #endregion ------------------------------------------------------------------ /** @@ -3437,7 +3407,9 @@ private static function _load_required_static() { add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 ); } - add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) ); + $clone_manager = FS_Clone_Manager::instance(); + add_action( 'init', array( $clone_manager, '_init' ) ); + add_action( 'admin_footer', array( 'Freemius', '_open_support_forum_in_new_page' ) ); if ( self::is_plugins_page() || self::is_themes_page() ) { @@ -3456,45 +3428,194 @@ private static function _load_required_static() { self::$_statics_loaded = true; } + #-------------------------------------------------------------------------------- + #region Clone + #-------------------------------------------------------------------------------- + /** * @author Leo Fajardo (@leorw) + * @since 2.5.0 * - * @since 2.1.3 + * @param bool $only_if_manual_resolution_is_not_hidden + * + * @return bool */ - private static function migrate_options_to_network() { - self::migrate_accounts_to_network(); - - // Migrate API options from site level to network level. - $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true ); - $api_network_options->migrate_to_network(); - - // Migrate API cache to network level storage. - FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network(); + private function is_unresolved_clone( $only_if_manual_resolution_is_not_hidden = false ) { + if ( ! $this->is_clone( $only_if_manual_resolution_is_not_hidden ) ) { + return false; + } - self::$_accounts->set_option( 'ms_migration_complete', true, true ); + return FS_Clone_Manager::instance()->has_temporary_duplicate_mode_expired(); } - #---------------------------------------------------------------------------------- - #region Localization - #---------------------------------------------------------------------------------- - /** - * Load framework's text domain. + * @author Leo Fajardo (@leorw) + * @since 2.5.0 * - * @author Vova Feldman (@svovaf) - * @since 1.2.1 + * @param bool $only_if_manual_resolution_is_not_hidden */ - static function _load_textdomain() { - if ( ! is_admin() ) { - return; + function is_clone( $only_if_manual_resolution_is_not_hidden = false ) { + if ( ! is_object( $this->_site ) ) { + return false; } - global $fs_active_plugins; + $blog_id = null; - // Works both for plugins and themes. - load_plugin_textdomain( - 'freemius', - false, + if ( + fs_is_network_admin() && + FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) + ) { + // Ensure that we're comparing the network install's URL with the relevant subsite's URL. + $blog_id = $this->_storage->network_install_blog_id; + } + + $site_url = Freemius::get_unfiltered_site_url( $blog_id, true, true ); + + if ( ! $this->_site->is_clone( $site_url ) ) { + return false; + } + + return ( + ! $only_if_manual_resolution_is_not_hidden || + ! FS_Clone_Manager::instance()->should_hide_manual_resolution() + ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param int|null $blog_id + * @param bool $strip_protocol + * @param bool $add_trailing_slash + * + * @return string + */ + static function get_unfiltered_site_url( $blog_id = null, $strip_protocol = false, $add_trailing_slash = false ) { + global $wp_filter; + + $site_url_filters = array( + 'site_url' => null, + 'pre_option_siteurl' => null, + 'default_option_siteurl' => null, + 'option_siteurl' => null, + ); + + // Detach all URL-related filters to get the actual site's URL (stripped of potential manipulations by multilingual plugins). + foreach ( $site_url_filters as $hook_name => $site_url_filter ) { + if ( ! empty( $wp_filter[ $hook_name ] ) ) { + $site_url_filters[ $hook_name ] = $wp_filter[ $hook_name ]; + unset( $wp_filter[ $hook_name ] ); + } + } + + $url = get_site_url( $blog_id ); + + // Re-attach the filters back. + foreach ( $site_url_filters as $hook_name => $site_url_filter ) { + if ( ! empty( $site_url_filter ) ) { + $wp_filter[ $hook_name ] = $site_url_filter; + } + } + + if ( $strip_protocol ) { + $url = fs_strip_url_protocol( $url ); + } + + if ( $add_trailing_slash ) { + $url = trailingslashit( $url ); + } + + return $url; + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param number $site_id + */ + function fetch_install_by_id( $site_id ) { + return $this->get_current_or_network_user_api_scope()->get( "/installs/{$site_id}.json" ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return string|object|bool + */ + function _handle_long_term_duplicate() { + $this->_logger->entrance(); + + $this->delete_current_install( false ); + + $license_key = false; + + if ( + is_object( $this->_license ) && + ! $this->_license->is_utilized( + ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) ) + ) + ) { + $license_key = $this->_license->secret_key; + } + + return $this->opt_in( + false, + false, + false, + $license_key, + false, + false, + false, + null, + array(), + false + ); + } + + #endregion + + /** + * @author Leo Fajardo (@leorw) + * + * @since 2.1.3 + */ + private static function migrate_options_to_network() { + self::migrate_accounts_to_network(); + + // Migrate API options from site level to network level. + $api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true ); + $api_network_options->migrate_to_network(); + + // Migrate API cache to network level storage. + FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network(); + + self::$_accounts->set_option( 'ms_migration_complete', true, true ); + } + + #---------------------------------------------------------------------------------- + #region Localization + #---------------------------------------------------------------------------------- + + /** + * Load framework's text domain. + * + * @author Vova Feldman (@svovaf) + * @since 1.2.1 + */ + static function _load_textdomain() { + if ( ! is_admin() ) { + return; + } + + global $fs_active_plugins; + + // Works both for plugins and themes. + load_plugin_textdomain( + 'freemius', + false, $fs_active_plugins->newest->sdk_path . '/languages/' ); } @@ -3531,7 +3652,7 @@ static function _add_debug_section() { } else { // Add hidden debug page. $hook = FS_Admin_Menu_Manager::add_subpage( - null, + '', $title, $title, 'manage_options', @@ -3638,7 +3759,7 @@ static function _set_db_option() { self::shoot_ajax_failure(); } - $option_value = fs_request_get( 'option_value' ); + $option_value = fs_request_get_raw( 'option_value' ); if ( ! empty( $option_value ) ) { update_option( $option_name, $option_value ); @@ -3692,6 +3813,10 @@ static function _debug_page_actions() { switch_to_blog( $current_blog_id ); } + } else if ( fs_request_is_action( 'reset_deactivation_snoozing' ) ) { + check_admin_referer( 'reset_deactivation_snoozing' ); + + self::reset_deactivation_snoozing(); } else if ( fs_request_is_action( 'simulate_trial' ) ) { check_admin_referer( 'simulate_trial' ); @@ -3740,55 +3865,69 @@ static function _debug_page_actions() { } /** - * @author Vova Feldman (@svovaf) - * @since 1.0.8 + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return array */ - static function _debug_page_render() { + static function get_all_modules_sites() { self::$_static_logger->entrance(); + $sites_by_type = array( + WP_FS__MODULE_TYPE_PLUGIN => array(), + WP_FS__MODULE_TYPE_THEME => array(), + ); + + $module_types = array_keys( $sites_by_type ); + if ( ! is_multisite() ) { - $all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN ); - $all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME ); + foreach ( $module_types as $type ) { + $sites_by_type[ $type ] = self::get_all_sites( $type ); + + foreach ( $sites_by_type[ $type ] as $slug => $install ) { + $sites_by_type[ $type ][ $slug ] = array( $install ); + } + } } else { $sites = self::get_sites(); - $all_plugins_installs = array(); - $all_themes_installs = array(); - foreach ( $sites as $site ) { $blog_id = self::get_site_blog_id( $site ); - $plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id ); - - foreach ( $plugins_installs as $slug => $install ) { - if ( ! isset( $all_plugins_installs[ $slug ] ) ) { - $all_plugins_installs[ $slug ] = array(); - } - - $install->blog_id = $blog_id; + foreach ( $module_types as $type ) { + $installs = self::get_all_sites( $type, $blog_id ); - $all_plugins_installs[ $slug ][] = $install; - } + foreach ( $installs as $slug => $install ) { + if ( ! isset( $sites_by_type[ $type ][ $slug ] ) ) { + $sites_by_type[ $type ][ $slug ] = array(); + } - $themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id ); + $install->blog_id = $blog_id; - foreach ( $themes_installs as $slug => $install ) { - if ( ! isset( $all_themes_installs[ $slug ] ) ) { - $all_themes_installs[ $slug ] = array(); + $sites_by_type[ $type ][ $slug ][] = $install; } - $install->blog_id = $blog_id; - - $all_themes_installs[ $slug ][] = $install; } } } + return $sites_by_type; + } + + /** + * @author Vova Feldman (@svovaf) + * @since 1.0.8 + */ + static function _debug_page_render() { + self::$_static_logger->entrance(); + + $all_modules_sites = self::get_all_modules_sites(); + $licenses_by_module_type = self::get_all_licenses_by_module_type(); $vars = array( - 'plugin_sites' => $all_plugins_installs, - 'theme_sites' => $all_themes_installs, + 'plugin_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_PLUGIN ], + 'theme_sites' => $all_modules_sites[ WP_FS__MODULE_TYPE_THEME ], 'users' => self::get_all_users(), 'addons' => self::get_all_addons(), 'account_addons' => self::get_all_account_addons(), @@ -3820,6 +3959,10 @@ static function _debug_page_render() { function is_on() { self::$_static_logger->entrance(); + if ( is_object( $this->_site ) && ! $this->is_registered() ) { + return false; + } + if ( isset( $this->_is_on ) ) { return $this->_is_on; } @@ -3891,36 +4034,58 @@ private function should_run_connectivity_test( $flush_if_no_connectivity = false } /** - * @author Vova Feldman (@svovaf) - * @since 1.1.7.4 + * @author Leo Fajardo (@leorw) + * @since 2.5.4 * - * @param int|null $blog_id Since 2.0.0. - * @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test. + * @param bool $is_update * - * @return object|false + * @return bool */ - private function ping( $blog_id = null, $is_gdpr_test = false ) { - if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) { + private function should_turn_fs_on( $is_update = true ) { + if ( + empty( $this->_plugin->opt_in_moderation ) || + ! is_array( $this->_plugin->opt_in_moderation ) + ) { + return true; + } + + $optin_config = $this->_plugin->opt_in_moderation; + + if ( + WP_FS__IS_LOCALHOST && + ( ! isset( $optin_config['localhost'] ) || false !== $optin_config['localhost'] ) + ) { + return true; + } + + $optin_config_key = $is_update ? + 'updates' : + 'new'; + + if ( ! isset( $optin_config[ $optin_config_key ] ) ) { + return true; + } + + $visibility_percentage = $optin_config[ $optin_config_key ]; + + if ( 0 == $visibility_percentage ) { return false; } - $version = $this->get_plugin_version(); + if ( ! is_numeric( $visibility_percentage ) ) { + return true; + } - $is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ); + $min = 1; + $max = 100; - return $this->get_api_plugin_scope()->ping( - $this->get_anonymous_id( $blog_id ), - array( - 'is_update' => json_encode( $is_update ), - 'version' => $version, - 'sdk' => $this->version, - 'is_admin' => json_encode( is_admin() ), - 'is_ajax' => json_encode( self::is_ajax() ), - 'is_cron' => json_encode( self::is_cron() ), - 'is_gdpr_test' => $is_gdpr_test, - 'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ), - ) - ); + if ( function_exists( 'random_int' ) ) { + $random = random_int( $min, $max ); + } else { + $random = rand( $min, $max ); + } + + return ( $random <= $visibility_percentage ); } /** @@ -3931,7 +4096,7 @@ private function ping( $blog_id = null, $is_gdpr_test = false ) { * * @param bool $flush_if_no_connectivity * - * @return bool + * @return bool|null */ function has_api_connectivity( $flush_if_no_connectivity = false ) { $this->_logger->entrance(); @@ -3944,7 +4109,7 @@ function has_api_connectivity( $flush_if_no_connectivity = false ) { isset( $this->_storage->connectivity_test ) && true === $this->_storage->connectivity_test['is_connected'] ) { - unset( $this->_storage->connectivity_test ); + $this->clear_connectivity_info(); } if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) { @@ -3961,36 +4126,47 @@ function has_api_connectivity( $flush_if_no_connectivity = false ) { return $this->_has_api_connection; } - $pong = $this->ping(); - $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong ); + if ( + ! empty( $this->_storage->connectivity_test ) && + isset( $this->_storage->connectivity_test['is_active'] ) + ) { + $is_active = $this->_storage->connectivity_test['is_active']; + } else { + $is_active = $this->should_turn_fs_on( $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() ) ); - if ( ! $is_connected ) { - // API failure. - $this->_add_connectivity_issue_message( $pong ); + $this->store_connectivity_info( (object) array( 'is_active' => $is_active ), null ); } - if ( $is_connected ) { - FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required ); + if ( $is_active ) { + $this->_is_on = true; } - - $this->store_connectivity_info( $pong, $is_connected ); return $this->_has_api_connection; } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + */ + private function clear_connectivity_info() { + unset( $this->_storage->connectivity_test ); + + FS_Api::clear_force_http_flag(); + } + /** * @author Vova Feldman (@svovaf) * @since 1.1.7.4 * - * @param object $pong - * @param bool $is_connected + * @param object $pong + * @param bool|null $is_connected */ private function store_connectivity_info( $pong, $is_connected ) { $this->_logger->entrance(); $version = $this->get_plugin_version(); - if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) { + if ( false === $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) { $is_active = false; } else { $is_active = ( isset( $pong->is_active ) && true == $pong->is_active ); @@ -4017,6 +4193,20 @@ private function store_connectivity_info( $pong, $is_connected ) { $this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF ); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param bool $is_connected + */ + private function update_connectivity_info( $is_connected ) { + $this->store_connectivity_info( + // This is true since we update the connection info only after a successful opt-in or license activation which means that Freemius has already been on even before the process. + (object) array( 'is_active' => true ), + $is_connected + ); + } + /** * Force turning Freemius on. * @@ -4056,7 +4246,7 @@ function get_anonymous_id( $blog_id = null ) { $unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id ); if ( empty( $unique_id ) || ! is_string( $unique_id ) ) { - $key = fs_strip_url_protocol( get_site_url( $blog_id ) ); + $key = self::get_unfiltered_site_url( $blog_id, true ); $secure_auth = defined( 'SECURE_AUTH_KEY' ) ? SECURE_AUTH_KEY : ''; if ( empty( $secure_auth ) || @@ -4085,6 +4275,17 @@ function get_anonymous_id( $blog_id = null ) { return $unique_id; } + /** + * Returns anonymous network ID. + * + * @since 2.4.3 + * + * @return string + */ + function get_anonymous_network_id() { + return $this->get_anonymous_id( get_network()->site_id ); + } + /** * @author Vova Feldman (@svovaf) * @since 1.1.7.4 @@ -4203,374 +4404,6 @@ static function is_valid_email( $email ) { return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) ); } - /** - * Generate API connectivity issue message. - * - * @author Vova Feldman (@svovaf) - * @since 1.0.9 - * - * @param mixed $api_result - * @param bool $is_first_failure - */ - function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) { - if ( ! $this->is_premium() && $this->_enable_anonymous ) { - // Don't add message if it's the free version and can run anonymously. - return; - } - - if ( ! function_exists( 'wp_nonce_url' ) ) { - require_once ABSPATH . 'wp-includes/functions.php'; - } - - $current_user = self::_get_current_wp_user(); -// $admin_email = get_option( 'admin_email' ); - $admin_email = $current_user->user_email; - - // Aliases. - $deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' ); - $deactivate_plugin_desc = $this->esc_html_inline( 'We feel your frustration and sincerely apologize for the inconvenience. Hope to see you again in the future.', 'deactivate-plugin-desc' ); - $install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' ); - $install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' ); - $fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' ); - $fix_issue_desc = $this->esc_html_inline( 'We will do our best to whitelist your server and resolve this issue ASAP. You will get a follow-up email to %s once we have an update.', 'fix-issue-desc' ); - /* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */ - $x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' ); - $sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' ); - $happy_to_resolve_issue_asap = $this->esc_html_inline( 'We are sure it\'s an issue on our side and more than happy to resolve it for you ASAP if you give us a chance.', 'happy-to-resolve-issue-asap' ); - - $message = false; - if ( is_object( $api_result ) && - isset( $api_result->error ) && - isset( $api_result->error->code ) - ) { - switch ( $api_result->error->code ) { - case 'curl_missing': - $missing_methods = ''; - if ( is_array( $api_result->missing_methods ) && - ! empty( $api_result->missing_methods ) - ) { - foreach ( $api_result->missing_methods as $m ) { - if ( 'curl_version' === $m ) { - continue; - } - - if ( ! empty( $missing_methods ) ) { - $missing_methods .= ', '; - } - - $missing_methods .= sprintf( '%s', $m ); - } - - if ( ! empty( $missing_methods ) ) { - $missing_methods = sprintf( - '

%s %s', - $this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ), - $missing_methods - ); - } - } - - $message = sprintf( - $x_requires_access_to_api . ' ' . - $this->esc_html_inline( 'We use PHP cURL library for the API calls, which is a very common library and usually installed and activated out of the box. Unfortunately, cURL is not activated (or disabled) on your server.', 'curl-missing-message' ) . ' ' . - $missing_methods . - ' %s', - '' . $this->get_plugin_name() . '', - sprintf( - '
  1. %s
  2. %s
  3. %s
', - sprintf( - '%s%s', - $this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ), - ' - ' . sprintf( - $this->get_text_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'curl-missing-no-clue-desc' ), - '' . $admin_email . '' - ) - ), - sprintf( - '%s - %s', - $sysadmin_title, - esc_html( sprintf( $this->get_text_inline( 'Great, please install cURL and enable it in your php.ini file. In addition, search for the \'disable_functions\' directive in your php.ini file and remove any disabled methods starting with \'curl_\'. To make sure it was successfully activated, use \'phpinfo()\'. Once activated, deactivate the %s and reactivate it back again.', 'curl-missing-sysadmin-desc' ), $this->get_module_label( true ) ) ) - ), - sprintf( - '%s - %s', - wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ), - $deactivate_plugin_title, - $deactivate_plugin_desc - ) - ) - ); - break; - case 'cloudflare_ddos_protection': - $message = sprintf( - $x_requires_access_to_api . ' ' . - $this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' . - $happy_to_resolve_issue_asap . - ' %s', - '' . $this->get_plugin_name() . '', - sprintf( - '
  1. %s
  2. %s
  3. %s
', - sprintf( - '%s%s', - $fix_issue_title, - ' - ' . sprintf( - $fix_issue_desc, - '' . $admin_email . '' - ) - ), - sprintf( - '%s - %s', - sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ), - $install_previous_title, - $install_previous_desc - ), - sprintf( - '%s - %s', - wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=' . '', 'deactivate-plugin_' . $this->_plugin_basename ), - $deactivate_plugin_title, - $deactivate_plugin_desc - ) - ) - ); - break; - case 'squid_cache_block': - $message = sprintf( - $x_requires_access_to_api . ' ' . - $this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) . - ' %s', - '' . $this->get_plugin_name() . '', - sprintf( - '
  1. %s
  2. %s
  3. %s
', - sprintf( - '%s - %s', - $this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ), - sprintf( - $this->esc_html_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'squid-no-clue-desc' ), - '' . $admin_email . '' - ) - ), - sprintf( - '%s - %s', - $sysadmin_title, - sprintf( - $this->esc_html_inline( 'Great, please whitelist the following domains: %s. Once you are done, deactivate the %s and activate it again.', 'squid-sysadmin-desc' ), - // We use a filter since the plugin might require additional API connectivity. - '' . implode( ', ', $this->apply_filters( 'api_domains', array( - 'api.freemius.com', - 'wp.freemius.com' - ) ) ) . '', - $this->_module_type - ) - ), - sprintf( - '%s - %s', - wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ), - $deactivate_plugin_title, - $deactivate_plugin_desc - ) - ) - ); - break; -// default: -// $message = $this->get_text_inline( 'connectivity-test-fails-message' ); -// break; - } - } - - $message_id = 'failed_connect_api'; - $type = 'error'; - - $connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' ); - - if ( false === $message ) { - if ( $is_first_failure ) { - // First attempt failed. - $message = sprintf( - $x_requires_access_to_api . ' ' . - $connectivity_test_fails_message . ' ' . - $this->esc_html_inline( 'It\'s probably a temporary issue on our end. Just to be sure, with your permission, would it be o.k to run another connectivity test?', 'connectivity-test-maybe-temporary' ) . '

' . - '%s', - '' . $this->get_plugin_name() . '', - sprintf( - '
%s %s
', - sprintf( - '%s', - $this->get_text_inline( 'Yes - do your thing', 'yes-do-your-thing' ) - ), - sprintf( - '%s', - wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ), - $this->get_text_inline( 'No - just deactivate', 'no-deactivate' ) - ) - ) - ); - - $message_id = 'failed_connect_api_first'; - $type = 'promotion'; - } else { - // Second connectivity attempt failed. - $message = sprintf( - $x_requires_access_to_api . ' ' . - $connectivity_test_fails_message . ' ' . - $happy_to_resolve_issue_asap . - ' %s', - '' . $this->get_plugin_name() . '', - sprintf( - '
  1. %s
  2. %s
  3. %s
', - sprintf( - '%s%s', - $fix_issue_title, - ' - ' . sprintf( - $fix_issue_desc, - '' . $admin_email . '' - ) - ), - sprintf( - '%s - %s', - sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ), - $install_previous_title, - $install_previous_desc - ), - sprintf( - '%s - %s', - wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ), - $deactivate_plugin_title, - $deactivate_plugin_desc - ) - ) - ); - } - } - - $this->_admin_notices->add_sticky( - $message, - $message_id, - $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...', - $type - ); - } - - /** - * Handle user request to resolve connectivity issue. - * This method will send an email to Freemius API technical staff for resolution. - * The email will contain server's info and installed plugins (might be caching issue). - * - * @author Vova Feldman (@svovaf) - * @since 1.0.9 - */ - function _email_about_firewall_issue() { - check_admin_referer( 'fs_resolve_firewall_issues' ); - - if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) { - return; - } - - $this->_admin_notices->remove_sticky( 'failed_connect_api' ); - - $pong = $this->ping(); - - $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong ); - - if ( $is_connected ) { - FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required ); - - $this->store_connectivity_info( $pong, $is_connected ); - - echo $this->get_after_plugin_activation_redirect_url(); - exit; - } - - $current_user = self::_get_current_wp_user(); - $admin_email = $current_user->user_email; - - $error_type = fs_request_get( 'error_type', 'general' ); - - switch ( $error_type ) { - case 'squid': - $title = 'Squid ACL Blocking Issue'; - break; - case 'cloudflare': - $title = 'CloudFlare Blocking Issue'; - break; - default: - $title = 'API Connectivity Issue'; - break; - } - - $custom_email_sections = array(); - - // Add 'API Error' custom email section. - $custom_email_sections['api_error'] = array( - 'title' => 'API Error', - 'rows' => array( - 'ping' => array( - 'API Error', - is_string( $pong ) ? htmlentities( $pong ) : json_encode( $pong ) - ), - ) - ); - - // Send email with technical details to resolve API connectivity issues. - $this->send_email( - 'api@freemius.com', // recipient - $title . ' [' . $this->get_plugin_name() . ']', // subject - $custom_email_sections, - array( "Reply-To: $admin_email <$admin_email>" ) // headers - ); - - $this->_admin_notices->add_sticky( - sprintf( - $this->get_text_inline( 'Thank for giving us the chance to fix it! A message was just sent to our technical staff. We will get back to you as soon as we have an update to %s. Appreciate your patience.', 'fix-request-sent-message' ), - '' . $admin_email . '' - ), - 'server_details_sent' - ); - - // Action was taken, tell that API connectivity troubleshooting should be off now. - - echo "1"; - exit; - } - - /** - * Handle connectivity test retry approved by the user. - * - * @author Vova Feldman (@svovaf) - * @since 1.1.7.4 - */ - function _retry_connectivity_test() { - check_admin_referer( 'fs_retry_connectivity_test' ); - - if ( ! current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) { - return; - } - - $this->_admin_notices->remove_sticky( 'failed_connect_api_first' ); - - $pong = $this->ping(); - - $is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong ); - - if ( $is_connected ) { - FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required ); - - $this->store_connectivity_info( $pong, $is_connected ); - - echo $this->get_after_plugin_activation_redirect_url(); - } else { - // Add connectivity issue message after 2nd failed attempt. - $this->_add_connectivity_issue_message( $pong, false ); - - echo "1"; - } - - exit; - } - - static function _add_firewall_issues_javascript() { - $params = array(); - fs_require_once_template( 'firewall-issues-js.php', $params ); - } - #endregion #---------------------------------------------------------------------------------- @@ -4805,6 +4638,36 @@ function dynamic_init( array $plugin_info ) { $this->register_after_settings_parse_hooks(); + /** + * If anonymous but there's already a user entity and the user's site is associated with a valid license or trial period, update the anonymous mode accordingly. + * + * @todo Remove this entire `if` block after several releases as starting from this version, the anonymous mode will already be updated accordingly after a purchase. + */ + if ( $this->is_anonymous() ) { + $is_network_level = ( $this->_is_network_active && fs_is_network_admin() ); + + if ( + ! $is_network_level || + FS_Site::is_valid_id( $this->_storage->network_install_blog_id ) + ) { + if ( $this->is_paying_or_trial() ) { + $this->reset_anonymous_mode( $is_network_level ); + } + } else { + $network = get_network(); + + if ( is_object( $network ) ) { + $main_blog_id = $network->site_id; + $first_install = $this->get_install_by_blog_id( $main_blog_id ); + + if ( is_object( $first_install ) ) { + $this->_storage->network_install_blog_id = $main_blog_id; + $this->_storage->network_user_id = $first_install->user_id; + } + } + } + } + if ( $this->should_stop_execution() ) { return; } @@ -4815,50 +4678,9 @@ function dynamic_init( array $plugin_info ) { $this->_has_api_connection = true; $this->_is_on = true; } else { - if ( ! $this->has_api_connectivity() ) { - if ( $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) || - $this->_admin_notices->has_sticky( 'failed_connect_api' ) - ) { - if ( ! $this->_enable_anonymous || $this->is_premium() ) { - // If anonymous mode is disabled, add firewall admin-notice message. - add_action( 'admin_footer', array( 'Freemius', '_add_firewall_issues_javascript' ) ); - - $ajax_action_suffix = $this->_slug . ( $this->is_theme() ? ':theme' : '' ); - add_action( "wp_ajax_fs_resolve_firewall_issues_{$ajax_action_suffix}", array( - &$this, - '_email_about_firewall_issue' - ) ); - - add_action( "wp_ajax_fs_retry_connectivity_test_{$ajax_action_suffix}", array( - &$this, - '_retry_connectivity_test' - ) ); - - /** - * Currently the admin notice manager relies on the module's type and slug. The new AJAX actions manager uses module IDs, hence, consider to replace the if block above with the commented code below after adjusting the admin notices manager to work with module IDs. - * - * @author Vova Feldman (@svovaf) - * @since 2.0.0 - */ - /*$this->add_ajax_action( 'resolve_firewall_issues', array( - &$this, - '_email_about_firewall_issue' - ) ); - - $this->add_ajax_action( 'retry_connectivity_test', array( - &$this, - '_retry_connectivity_test' - ) );*/ - } - } - + if ( false === $this->has_api_connectivity() ) { return; } else { - $this->_admin_notices->remove_sticky( array( - 'failed_connect_api_first', - 'failed_connect_api', - ) ); - if ( $this->_anonymous_mode ) { // Simulate anonymous mode. $this->_is_anonymous = true; @@ -4869,7 +4691,7 @@ function dynamic_init( array $plugin_info ) { /** * This should be executed even if Freemius is off for the core module, - * otherwise, the add-ons dialogbox won't work properly. This is esepcially + * otherwise, the add-ons dialog box won't work properly. This is especially * relevant when the developer decided to turn FS off for existing users. * * @author Vova Feldman (@svovaf) @@ -4907,22 +4729,25 @@ function dynamic_init( array $plugin_info ) { * @since 1.1.7.3 * */ - if ( $this->is_registered() ) { - if ( ! $this->is_sync_cron_on() && $this->is_tracking_allowed() ) { - $this->schedule_sync_cron(); - } + if ( $this->is_registered() && $this->is_tracking_allowed() ) { + $this->maybe_schedule_sync_cron(); } /** * Check if requested for manual blocking background sync. */ if ( fs_request_has( 'background_sync' ) ) { + self::require_pluggable_essentials(); + self::wp_cookie_constants(); + $this->run_manual_sync(); } } } if ( $this->is_registered() ) { + FS_Clone_Manager::instance()->maybe_resolve_new_subsite_install_automatically( $this ); + $this->hook_callback_to_install_sync(); } @@ -4937,6 +4762,28 @@ function dynamic_init( array $plugin_info ) { } if ( $this->is_user_in_admin() ) { + if ( $this->is_registered() && fs_request_has( 'purchase_completed' ) ) { + $this->_admin_notices->add_sticky( + sprintf( + /* translators: %s: License type (e.g. you have a professional license) */ + $this->get_text_inline( 'You have purchased a %s license.', 'you-have-x-license' ), + fs_request_get( 'purchased_plan' ) + ) . + sprintf( + $this->get_text_inline(" The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box.", 'post-purchase-email-sent-message' ), + $this->get_module_label( true ), + ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? "products' " : '' ), + ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? 's' : '' ), + sprintf( + '%s', + fs_request_get( 'purchase_email' ) + ) + ), + 'plan_purchased', + $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + ); + } + if ( $this->is_addon() ) { if ( ! $this->is_parent_plugin_installed() ) { $parent_name = $this->get_option( $plugin_info, 'parent_name', null ); @@ -5041,7 +4888,7 @@ function dynamic_init( array $plugin_info ) { * because the updater has some logic that needs to be executed * during AJAX calls. * - * Currently we need to hook to the `http_request_host_is_external` filter. + * Currently, we need to hook to the `http_request_host_is_external` filter. * In the future, there might be additional logic added. * * @author Vova Feldman @@ -5060,7 +4907,8 @@ function dynamic_init( array $plugin_info ) { */ ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $this->premium_plugin_basename() ) ) ) ) && - $this->has_release_on_freemius() + $this->has_release_on_freemius() && + ( ! $this->is_unresolved_clone( true ) ) ) { FS_Plugin_Updater::instance( $this ); } @@ -5100,179 +4948,99 @@ function dynamic_init( array $plugin_info ) { $this->do_action( 'after_init_addon_anonymous' ); } else if ( $this->is_pending_activation() ) { $this->do_action( 'after_init_addon_pending_activations' ); - } - } - } - - /** - * @author Leo Fajardo (@leorw) - * @since 2.2.3 - * - * @return bool - */ - private function should_use_freemius_updater_and_dialog() { - return ( - /** - * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true` - * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action` - * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts - * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing - * plugin details from .org). - */ - ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) || - ( - ! self::is_plugin_install_page() && - // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected. - ( 'install-plugin' !== fs_request_get( 'action' ) ) - ) - ); - } - - /** - * @author Leo Fajardo (@leorw) - * - * @since 1.2.1.5 - */ - function _stop_tracking_callback() { - $this->_logger->entrance(); - - $this->check_ajax_referer( 'stop_tracking' ); - - $result = $this->stop_tracking( fs_is_network_admin() ); - - if ( true === $result ) { - self::shoot_ajax_success(); - } - - $this->_logger->api_error( $result ); - - self::shoot_ajax_failure( - sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) . - ( $this->is_api_error( $result ) && isset( $result->error ) ? - $result->error->message : - var_export( $result, true ) ) - ); - } - - /** - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 - */ - function _allow_tracking_callback() { - $this->_logger->entrance(); - - $this->check_ajax_referer( 'allow_tracking' ); - - $result = $this->allow_tracking( fs_is_network_admin() ); - - if ( true === $result ) { - self::shoot_ajax_success(); + } } + } - $this->_logger->api_error( $result ); - - self::shoot_ajax_failure( - sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) . - ( $this->is_api_error( $result ) && isset( $result->error ) ? - $result->error->message : - var_export( $result, true ) ) + /** + * @author Leo Fajardo (@leorw) + * @since 2.2.3 + * + * @return bool + */ + private function should_use_freemius_updater_and_dialog() { + return ( + /** + * Allow updater and dialog when the `fs_allow_updater_and_dialog` URL query param exists and has `true` + * value, or when the current page is not the "Add Plugins" page (/plugin-install.php) and the `action` + * URL query param doesn't exist or its value is not `install-plugin` so that there will be no conflicts + * with the .org plugins' functionalities (e.g. installation from the "Add Plugins" page and viewing + * plugin details from .org). + */ + ( true === fs_request_get_bool( 'fs_allow_updater_and_dialog' ) ) || + ( + ! self::is_plugin_install_page() && + // Disallow updater and dialog when installing a plugin, otherwise .org "add-on" plugins will be affected. + ( 'install-plugin' !== fs_request_get( 'action' ) ) + ) ); } /** - * Opt-out from usage tracking. - * - * Note: This will not delete the account information but will stop all tracking. - * - * Returns: - * 1. FALSE - If the user never opted-in. - * 2. TRUE - If successfully opted-out. - * 3. object - API result on failure. - * - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 + * @param string[] $permissions + * @param bool $is_enabled + * @param int|null $blog_id * - * @return bool|object + * @return true|object `true` on success, API error object on failure. */ - function stop_site_tracking() { + private function update_site_permissions( array $permissions, $is_enabled, $blog_id = null ) { $this->_logger->entrance(); - if ( ! $this->is_registered() ) { - // User never opted-in. - return false; - } + $params = array( + 'permissions' => implode( ',', $permissions ), + 'is_enabled' => $is_enabled, + ); - if ( $this->is_tracking_prohibited() ) { - // Already disconnected. - return true; + $current_blog_id = get_current_blog_id(); + $is_blog_switched = false; + if ( is_numeric( $blog_id ) && $current_blog_id != $blog_id ) { + $is_blog_switched = $this->switch_to_blog( $blog_id ); } - // Send update to FS. - $result = $this->get_api_site_scope()->call( '/?fields=is_disconnected', 'put', array( - 'is_disconnected' => true - ) ); + $result = $this->api_site_call( '/permissions.json', 'put', $params ); + + if ( $is_blog_switched ) { + $this->switch_to_blog( $current_blog_id ); + } - if ( ! $this->is_api_result_entity( $result ) || - ! isset( $result->is_disconnected ) || - ! $result->is_disconnected + if ( + ! $this->is_api_result_object( $result ) || + ! isset( $result->install_id ) ) { $this->_logger->api_error( $result ); return $result; } - $this->_site->is_disconnected = $result->is_disconnected; - $this->_store_site(); - - $this->clear_sync_cron(); - - // Successfully disconnected. return true; } /** - * Opt-out network from usage tracking. - * - * Note: This will not delete the account information but will stop all tracking. - * - * Returns: - * 1. FALSE - If the user never opted-in. - * 2. TRUE - If successfully opted-out. - * 3. object - API result on failure. - * - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 + * @param string[] $permissions + * @param bool $is_enabled + * @param bool $has_site_delegated_connection * - * @return bool|object + * @return true|object `true` on success, API error object on failure. */ - function stop_network_tracking() { + private function update_network_permissions( + array $permissions, + $is_enabled, + &$has_site_delegated_connection + ) { $this->_logger->entrance(); - if ( ! $this->is_registered() ) { - // User never opted-in. - return false; - } - $install_id_2_blog_id = array(); - $installs_map = $this->get_blog_install_map(); - - $opt_out_all = true; + $install_by_blog_id = $this->get_blog_install_map(); - $params = array(); - foreach ( $installs_map as $blog_id => $install ) { - if ( $install->is_tracking_prohibited() ) { - // Already opted-out. - continue; - } + $has_site_delegated_connection = false; + foreach ( $install_by_blog_id as $blog_id => $install ) { if ( $this->is_site_delegated_connection( $blog_id ) ) { - // Opt-out only from non-delegated installs. - $opt_out_all = false; + // Only update permissions of non-delegated installs. + $has_site_delegated_connection = true; continue; } - $params[] = array( 'id' => $install->id ); - $install_id_2_blog_id[ $install->id ] = $blog_id; } @@ -5280,171 +5048,188 @@ function stop_network_tracking() { return true; } - $params[] = array( 'is_disconnected' => true ); + $params = array( + 'permissions' => implode( ',', $permissions ), + 'is_enabled' => $is_enabled, + 'install_ids' => implode( ',', array_keys( $install_id_2_blog_id ) ), + ); // Send update to FS. - $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params ); + $result = $this->get_current_or_network_user_api_scope()->call( + "/plugins/{$this->_module_id}/installs/permissions.json", + 'put', + $params + ); - if ( ! $this->is_api_result_object( $result, 'installs' ) ) { + if ( ! $this->is_api_result_object( $result, 'installs_metadata' ) ) { $this->_logger->api_error( $result ); return $result; } - foreach ( $result->installs as $r_install ) { - $blog_id = $install_id_2_blog_id[ $r_install->id ]; - $install = $installs_map[ $blog_id ]; - $install->is_disconnected = $r_install->is_disconnected; - $this->_store_site( true, $blog_id, $install ); - } - - $this->clear_sync_cron( $opt_out_all ); - - // Successfully disconnected. return true; } /** - * Opt-out from usage tracking. - * - * Note: This will not delete the account information but will stop all tracking. - * - * Returns: - * 1. FALSE - If the user never opted-in. - * 2. TRUE - If successfully opted-out. - * 3. object - API result on failure. - * - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 - * - * @param bool $is_network_action + * @param mixed $result * - * @return bool|object + * @return string */ - function stop_tracking( $is_network_action = false ) { - $this->_logger->entrance(); + private function get_api_error_message( $result ) { + $error_message = sprintf( $this->get_text_inline( 'There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn\'t work, contact the %s\'s author with the following:', + 'unexpected-api-error' ), $this->_module_type ) . ' '; + + if ( + $this->is_api_error( $result ) && + isset( $result->error ) + ) { + $code = empty( $result->error->code ) ? '' : " Code: {$result->error->code}"; + + $error_message .= "{$result->error->message}{$code}"; + } else { + $error_message .= var_export( $result, true ); + } - return $is_network_action ? - $this->stop_network_tracking() : - $this->stop_site_tracking(); + return $error_message; } /** - * Opt-in back into usage tracking. - * - * Note: This will only work if the user opted-in previously. - * - * Returns: - * 1. FALSE - If the user never opted-in. - * 2. TRUE - If successfully opted-in back to usage tracking. - * 3. object - API result on failure. - * - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 - * - * @return bool|object + * @author Vova Feldman (@svovaf) + * @since 2.5.1 */ - function allow_site_tracking() { + function _toggle_permission_tracking_callback() { $this->_logger->entrance(); - if ( ! $this->is_registered() ) { - // User never opted-in. - return false; - } + $this->check_ajax_referer( 'toggle_permission_tracking' ); - if ( $this->is_tracking_allowed() ) { - // Tracking already allowed. - return true; + if ( ! $this->is_registered( true ) ) { + self::shoot_ajax_failure( 'User never opted-in.' ); } - $result = $this->get_api_site_scope()->call( '/?is_disconnected', 'put', array( - 'is_disconnected' => false - ) ); - - if ( ! $this->is_api_result_entity( $result ) || - ! isset( $result->is_disconnected ) || - $result->is_disconnected - ) { - $this->_logger->api_error( $result ); + $is_enabled = fs_request_get_bool( 'is_enabled' ); + $permissions = fs_request_get( 'permissions' ); - return $result; + if ( ! is_string( $permissions ) ) { + self::shoot_ajax_failure( 'The permissions param must be a string.' ); } - $this->_site->is_disconnected = $result->is_disconnected; - $this->_store_site(); + $permissions = explode( ',', $permissions ); - $this->schedule_sync_cron(); + $result = $this->toggle_permission_tracking( $permissions, $is_enabled ); - // Successfully reconnected. - return true; + if ( true !== $result ) { + self::shoot_ajax_failure( $this->get_api_error_message( $result ) ); + } + + self::shoot_ajax_success(); } /** - * Opt-in network back into usage tracking. - * - * Note: This will only work if the user opted-in previously. - * - * Returns: - * 1. FALSE - If the user never opted-in. - * 2. TRUE - If successfully opted-in back to usage tracking. - * 3. object - API result on failure. - * - * @author Leo Fajardo (@leorw) - * @since 1.2.1.5 + * @param string[] $permissions + * @param bool $is_enabled + * @param int|null $blog_id * - * @return bool|object + * @return bool|mixed `true` if updated successfully or no update is needed. */ - function allow_network_tracking() { - $this->_logger->entrance(); - - if ( ! $this->is_registered() ) { + private function toggle_permission_tracking( $permissions, $is_enabled, $blog_id = null ) { + if ( ! $this->is_registered( true ) ) { // User never opted-in. return false; } - $install_id_2_blog_id = array(); - $installs_map = $this->get_blog_install_map(); - - $params = array(); - foreach ( $installs_map as $blog_id => $install ) { - if ( $install->is_tracking_allowed() ) { - continue; - } - - $params[] = array( 'id' => $install->id ); - - $install_id_2_blog_id[ $install->id ] = $blog_id; + // Check if permissions are already set as needed. + if ( FS_Permission_Manager::instance( $this )->are_permissions( $permissions, $is_enabled, $blog_id ) ) { + /** + * Note: + * When running on the network admin, there's no need to iterate through all the installs individually since network opt-in permissions are managed for ALL non-delegated installs through a single option (per permission) on the network-level storage. + */ + return true; } - if ( empty( $install_id_2_blog_id ) ) { - return true; + $api_managed_permissions = array_intersect( + $permissions, + FS_Permission_Manager::get_api_managed_permission_ids() + ); + + if ( + in_array( FS_Permission_Manager::PERMISSION_ESSENTIALS, $permissions ) && + ! in_array( FS_Permission_Manager::PERMISSION_SITE, $permissions ) + ) { + $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_SITE; } - $params[] = array( 'is_disconnected' => false ); + if ( ! empty( $api_managed_permissions ) ) { + $has_site_delegated_connection = false; - // Send update to FS. - $result = $this->get_current_or_network_user_api_scope()->call( "/plugins/{$this->_module_id}/installs.json", 'put', $params ); + if ( + ! $is_enabled && + ! in_array( FS_Permission_Manager::PERMISSION_EXTENSIONS, $api_managed_permissions ) && + false === FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed( $blog_id ) + ) { + /** + * If we are turning off a permission and the extensions permission is off too, enrich the permissions update request to also turn off extensions tracking, as currently when opting in with extensions tracking disabled the extensions tracking is off but the API isn't aware of it. + * + * @todo Remove this entire `if` after implementing granular opt-in that also sends the permissions to the API when opting in. + */ + $api_managed_permissions[] = FS_Permission_Manager::PERMISSION_EXTENSIONS; + } + if ( is_null( $blog_id ) && fs_is_network_admin() ) { + $result = $this->update_network_permissions( + $api_managed_permissions, + $is_enabled, + $has_site_delegated_connection + ); + } else { + $result = $this->update_site_permissions( + $api_managed_permissions, + $is_enabled, + $blog_id + ); + } - if ( ! $this->is_api_result_object( $result, 'installs' ) ) { - $this->_logger->api_error( $result ); + if ( true !== $result ) { + return $result; + } - return $result; - } + if ( in_array( FS_Permission_Manager::PERMISSION_SITE, $api_managed_permissions ) ) { + if ( $is_enabled ) { + $this->schedule_sync_cron(); + } else { + $this->clear_sync_cron( ! $has_site_delegated_connection ); + } + } - foreach ( $result->installs as $r_install ) { - $blog_id = $install_id_2_blog_id[ $r_install->id ]; - $install = $installs_map[ $blog_id ]; - $install->is_disconnected = $r_install->is_disconnected; - $this->_store_site( true, $blog_id, $install ); + if ( in_array( FS_Permission_Manager::PERMISSION_USER, $api_managed_permissions ) ) { + $this->toggle_user_permission( $is_enabled, $blog_id ); + } } - $this->schedule_sync_cron(); + $this->update_tracking_permissions( + $permissions, + $is_enabled, + $blog_id + ); - // Successfully reconnected. return true; } + /** + * @param bool $is_enabled + * @param int|null $blog_id + */ + private function toggle_user_permission( $is_enabled, $blog_id = null ) { + $network_or_blog_ids = is_numeric( $blog_id ) ? + $blog_id : + fs_is_network_admin(); + + if ( $is_enabled ) { + $this->reset_anonymous_mode( $network_or_blog_ids ); + } else { + $this->skip_connection( $network_or_blog_ids ); + } + } + /** * Opt-in back into usage tracking. * @@ -5458,16 +5243,18 @@ function allow_network_tracking() { * @author Leo Fajardo (@leorw) * @since 1.2.1.5 * - * @param bool $is_network_action + * @bool $is_enabled * * @return bool|object */ - function allow_tracking( $is_network_action = false ) { + private function toggle_site_tracking( $is_enabled, $blog_id = null ) { $this->_logger->entrance(); - return $is_network_action ? - $this->allow_network_tracking() : - $this->allow_site_tracking(); + return $this->toggle_permission_tracking( + FS_Permission_Manager::instance( $this )->get_site_tracking_permission_names(), + $is_enabled, + $blog_id + ); } /** @@ -5488,8 +5275,7 @@ private function reconnect_locally( $is_context_single_site = false ) { if ( ! fs_is_network_admin() || $is_context_single_site ) { if ( $this->is_tracking_prohibited() ) { - $this->_site->is_disconnected = false; - $this->_store_site(); + FS_Permission_Manager::instance( $this )->update_site_tracking( true ); } } else { $installs_map = $this->get_blog_install_map(); @@ -5497,73 +5283,69 @@ private function reconnect_locally( $is_context_single_site = false ) { /** * @var FS_Site $install */ - if ( $install->is_tracking_prohibited() ) { - $install->is_disconnected = false; - $this->_store_site( true, $blog_id, $install ); + if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) { + FS_Permission_Manager::instance( $this )->update_site_tracking( true, $blog_id ); } } } } /** - * @author Vova Feldman (@svovaf) - * @since 2.3.2 + * Update permission tracking flags. When updating in a network context, in addition to updating the network-level flags, also update the permissions on the site-level for all non-delegated sites. * - * @return bool + * @param string[] $permissions + * @param bool $is_enabled + * @param int|null $blog_id + * + * @return array */ - function is_extensions_tracking_allowed() { - return ( true === $this->apply_filters( - 'is_extensions_tracking_allowed', - $this->_storage->get( 'is_extensions_tracking_allowed', null ) - ) ); - } + private function update_tracking_permissions( $permissions, $is_enabled, $blog_id = null ) { + // Alias. + $permission_manager = FS_Permission_Manager::instance( $this ); - /** - * @author Vova Feldman (@svovaf) - * @since 2.3.2 - */ - function _update_tracking_permission_callback() { - $this->_logger->entrance(); + $network_or_blog_ids = is_numeric( $blog_id ) ? + $blog_id : + fs_is_network_admin(); - $this->check_ajax_referer( 'update_tracking_permission' ); + if ( true === $network_or_blog_ids ) { + // Update the permission for all non-delegated sub-sites. + $blog_ids = $this->get_non_delegated_blog_ids(); - $is_enabled = fs_request_get_bool( 'is_enabled', null ); + // Add the network-level to the array, to update the permission on the network-level storage. + array_unshift( $blog_ids, null ); + } + else + { + if ( false === $network_or_blog_ids ) { + $network_or_blog_ids = null; + } - if ( ! is_bool( $is_enabled ) ) { - self::shoot_ajax_failure(); + $blog_ids = is_array( $network_or_blog_ids ) ? + $network_or_blog_ids : + array( $network_or_blog_ids ); } - $permission = fs_request_get( 'permission' ); + $result = array(); + foreach ( $permissions as $permission ) { + $permission = trim( $permission ); + $is_permission_supported = true; + + foreach ( $blog_ids as $id ) { + $is_permission_supported = $permission_manager->update_permission_tracking_flag( + $permission, + $is_enabled, + $id + ); + } - switch ( $permission ) { - case 'extensions': - $this->update_extensions_tracking_flag( $is_enabled ); - break; - default: + if ( ! $is_permission_supported ) { $permission = 'no_match'; - } + } - if ( 'no_match' === $permission ) { - self::shoot_ajax_failure(); + $result[ $permission ] = $is_enabled; } - self::shoot_ajax_success( array( - 'permissions' => array( - $permission => $is_enabled, - ) - ) ); - } - - /** - * @author Leo Fajardo (@leorw) - * @since 2.3.2 - * - * @param bool|null $is_enabled - */ - function update_extensions_tracking_flag( $is_enabled ) { - if ( is_bool( $is_enabled ) ) { - $this->_storage->store( 'is_extensions_tracking_allowed', $is_enabled ); - } + return $result; } /** @@ -5643,6 +5425,7 @@ private function parse_settings( &$plugin_info ) { 'affiliate_moderation' => $this->get_option( $plugin_info, 'has_affiliation' ), 'bundle_id' => $this->get_option( $plugin_info, 'bundle_id', null ), 'bundle_public_key' => $this->get_option( $plugin_info, 'bundle_public_key', null ), + 'opt_in_moderation' => $this->get_option( $plugin_info, 'opt_in', null ), ) ); if ( $plugin->is_updated() ) { @@ -5769,8 +5552,7 @@ private function should_stop_execution() { if ( $this->is_activation_mode() ) { if ( ! is_admin() ) { /** - * If in activation mode, don't execute Freemius outside of the - * admin dashboard. + * If in activation mode, don't execute Freemius outside the admin dashboard. * * @author Vova Feldman (@svovaf) * @since 1.1.7.3 @@ -5802,10 +5584,7 @@ private function should_stop_execution() { return true; } - if ( self::is_ajax() && - ! $this->_admin_notices->has_sticky( 'failed_connect_api_first' ) && - ! $this->_admin_notices->has_sticky( 'failed_connect_api' ) - ) { + if ( self::is_ajax() ) { /** * During activation, if running in AJAX mode, unless there's a sticky * connectivity issue notice, don't run Freemius. @@ -5901,14 +5680,13 @@ function _plugin_code_type_changed() { $this->do_action( 'after_free_version_reactivation' ); if ( $this->is_paying() && ! $this->is_premium() ) { - $this->_admin_notices->add_sticky( + $this->add_complete_upgrade_instructions_notice( sprintf( /* translators: %s: License type (e.g. you have a professional license) */ $this->get_text_inline( 'You have a %s license.', 'you-have-x-license' ), $this->get_plan_title() - ) . $this->get_complete_upgrade_instructions(), - 'plan_upgraded', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + ), + 'plan_upgraded' ); } } @@ -6317,7 +6095,25 @@ function is_anonymous() { if ( ! isset( $this->_is_anonymous ) ) { if ( $this->is_network_anonymous() ) { $this->_is_anonymous = true; - } else if ( ! fs_is_network_admin() ) { + } else if ( fs_is_network_admin() ) { + /** + * When not-network-anonymous, yet, running in the network admin, consider as anonymous only when ALL non-delegated sites are set to anonymous. + */ + $non_delegated_sites = $this->get_non_delegated_blog_ids(); + + foreach ( $non_delegated_sites as $blog_id ) { + $is_anonymous = $this->_storage->get( 'is_anonymous', false, $blog_id ); + + if ( empty( $is_anonymous ) || false === $is_anonymous[ 'is' ] ) { + $this->_is_anonymous = false; + break; + } + } + + if ( false !== $this->_is_anonymous ) { + $this->_is_anonymous = true; + } + } else { if ( ! isset( $this->_storage->is_anonymous ) ) { // Not skipped. $this->_is_anonymous = false; @@ -6373,6 +6169,18 @@ function is_pending_activation() { return $this->_storage->get( 'is_pending_activation', false ); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + private function clear_pending_activation_mode() { + // Remove the pending activation sticky notice (if it still exists). + $this->_admin_notices->remove_sticky( 'activation_pending' ); + + // Clear the plugin's pending activation mode. + unset( $this->_storage->is_pending_activation ); + } + /** * Check if plugin must be WordPress.org compliant. * @@ -6449,9 +6257,11 @@ private function set_cron_data( $name, $cron_blog_id = 0 ) { private function get_cron_blog_id( $name ) { $this->_logger->entrance( $name ); - /** - * @var object $cron_data - */ + if ( ! is_multisite() ) { + // Not a multisite. + return 0; + } + $cron_data = $this->get_cron_data( $name ); return ( is_object( $cron_data ) && is_numeric( $cron_data->blog_id ) ) ? @@ -6562,18 +6372,23 @@ private function get_cron_target_blog_id( $except_blog_id = 0 ) { return 0; } - if ( $this->_is_network_active && - is_numeric( $this->_storage->network_install_blog_id ) && - $except_blog_id != $this->_storage->network_install_blog_id && - self::is_site_active( $this->_storage->network_install_blog_id ) - ) { - // Try to run cron from the main network blog. - $install = $this->get_install_by_blog_id( $this->_storage->network_install_blog_id ); + if ( $this->_is_network_active ) { + $network_install_blog_id = $this->_storage->network_install_blog_id; - if ( is_object( $install ) && - ( $this->is_premium() || $install->is_tracking_allowed() ) + if ( + is_numeric( $network_install_blog_id ) && + $except_blog_id != $network_install_blog_id && + self::is_site_active( $network_install_blog_id ) ) { - return $this->_storage->network_install_blog_id; + // Try to run cron from the main network blog. + $install = $this->get_install_by_blog_id( $network_install_blog_id ); + + if ( + is_object( $install ) && + $this->is_tracking_allowed( $network_install_blog_id, $install ) + ) { + return $network_install_blog_id; + } } } @@ -6582,7 +6397,7 @@ private function get_cron_target_blog_id( $except_blog_id = 0 ) { foreach ( $installs as $blog_id => $install ) { if ( $except_blog_id != $blog_id && self::is_site_active( $blog_id ) && - ( $this->is_premium() || $install->is_tracking_allowed() ) + $this->is_tracking_allowed( $blog_id, $install ) ) { return $blog_id; } @@ -6614,7 +6429,7 @@ private function clear_cron( $name, $action_tag = '', $is_network_clear = false /** * @var FS_Site $install */ - if ( $install->is_tracking_allowed() ) { + if ( $this->is_tracking_allowed( $blog_id, $install ) ) { $clear_cron = false; break; } @@ -6625,14 +6440,7 @@ private function clear_cron( $name, $action_tag = '', $is_network_clear = false return; } - /** - * @var object $cron_data - */ - $cron_data = $this->get_cron_data( $name ); - - $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ? - $cron_data->blog_id : - 0; + $cron_blog_id = $this->get_cron_blog_id( $name ); $this->clear_cron_data( $name ); @@ -6669,14 +6477,7 @@ private function get_next_scheduled_cron( $name, $action_tag = '' ) { return false; } - /** - * @var object $cron_data - */ - $cron_data = $this->get_cron_data( $name ); - - $cron_blog_id = is_object( $cron_data ) && isset( $cron_data->blog_id ) ? - $cron_data->blog_id : - 0; + $cron_blog_id = $this->get_cron_blog_id( $name ); if ( 0 < $cron_blog_id ) { switch_to_blog( $cron_blog_id ); @@ -6787,7 +6588,7 @@ private function execute_cron( $name, $callable ) { } else { $installs = $this->get_blog_install_map(); foreach ( $installs as $blog_id => $install ) { - if ( $this->is_premium() || $install->is_tracking_allowed() ) { + if ( $this->is_tracking_allowed( $blog_id, $install ) ) { if ( ! isset( $users_2_blog_ids[ $install->user_id ] ) ) { $users_2_blog_ids[ $install->user_id ] = array(); } @@ -6852,8 +6653,6 @@ private function get_sync_cron_blog_id() { * @since 1.1.7.3 */ private function run_manual_sync() { - self::require_pluggable_essentials(); - if ( ! $this->is_user_admin() ) { return; } @@ -6937,6 +6736,24 @@ private function is_sync_cron_on() { return $this->is_cron_on( 'sync' ); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + private function maybe_schedule_sync_cron() { + $next_schedule = $this->next_sync_cron(); + + // The event is properly scheduled, so no need to reschedule it. + if ( + is_numeric( $next_schedule ) && + $next_schedule > time() + ) { + return; + } + + $this->schedule_sync_cron(); + } + /** * @author Vova Feldman (@svovaf) * @since 1.1.7.3 @@ -7043,6 +6860,10 @@ private function get_install_sync_cron_blog_id() { * @param int $except_blog_id Since 2.0.0 when running in a multisite network environment, the cron execution is consolidated. This param allows excluding excluded specified blog ID from being the cron executor. */ private function schedule_install_sync( $except_blog_id = 0 ) { + if ( $this->is_clone() ) { + return; + } + $this->schedule_cron( 'install_sync', 'install_sync', 'single', WP_FS__SCRIPT_START_TIME, false, $except_blog_id ); } @@ -7131,29 +6952,92 @@ function _sync_install_cron_method( array $blog_ids, $current_blog_id = null ) { /** * Show a notice that activation is currently pending. * + * @todo Add some sort of mechanism to allow users to update the email address they would like to opt-in with when $is_suspicious_email is true. + * * @author Vova Feldman (@svovaf) * @since 1.0.7 * - * @param bool|string $email + * @param bool|string $email_address * @param bool $is_pending_trial Since 1.2.1.5 - */ - function _add_pending_activation_notice( $email = false, $is_pending_trial = false ) { - if ( ! is_string( $email ) ) { - $current_user = self::_get_current_wp_user(); - $email = $current_user->user_email; + * @param bool $is_suspicious_email Since 2.5.0 Set to true when there's an indication that email address the user opted in with is fake/dummy/placeholder. + * @param bool $has_upgrade_context Since 2.5.3 + * @param bool $support_email_address Since 2.5.3 + */ + function _add_pending_activation_notice( + $email_address = false, + $is_pending_trial = false, + $is_suspicious_email = false, + $has_upgrade_context = false, + $support_email_address = false + ) { + if ( ! is_string( $email_address ) ) { + $current_user = self::_get_current_wp_user(); + $email_address = $current_user->user_email; + } + + $formatted_message_args = array( + "{$this->get_plugin_name()}", + "{$email_address}", + ); + + if ( ! $has_upgrade_context || ! fs_is_network_admin() ) { + /* translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") */ + $formatted_message = $this->get_text_inline( 'You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s.', 'pending-activation-message' ); + + $formatted_message_args[] = $is_pending_trial ? + $this->get_text_inline( 'start the trial', 'start-the-trial' ) : + $this->get_text_inline( 'complete the opt-in', 'complete-the-opt-in' ); + + $notice_title = $this->get_text_inline( 'Thanks!', 'thanks' ); + } else { + /* translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") */ + $formatted_message = $this->get_text_inline( 'You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes.' ); + + if ( $this->has_release_on_freemius() ) { + $formatted_message_args[] = $this->get_text_x_inline( + 'the installation instructions', + 'Part of the message telling the user what they should receive via email.', + 'the-installation-instructions-phrase' + ); + } else { + $formatted_message_args[] = $this->get_text_x_inline( + 'a license key', + 'Part of the message telling the user what they should receive via email.', + 'a-license-key-phrase' + ); + + $formatted_message .= ( ' ' . sprintf( + /* translators: %s: activation link (e.g.: Click here) */ + $this->get_text_inline( '%s to activate the license once you get it.', 'license-activation-link-message' ), + sprintf( + '%s', + $this->get_activation_url( array( + 'fs_action' => 'reset_pending_activation_mode', + 'require_license' => 'true', + 'fs_unique_affix' => $this->get_unique_affix(), + ) ), + $this->get_text_x_inline( 'Click here', 'Part of an activation link message.', 'click-here' ) + ) + ) ); + } + + $formatted_message_args[] = ( ! empty( $support_email_address ) ) ? + ( "{$support_email_address}" ) : + $this->get_text_x_inline( + "the product's support email address", + 'Part of the message that tells the user to check their spam folder for a specific email.', + 'product-support-email-address-phrase' + ); + + $formatted_message .= ( ' ' . $this->get_text_inline( 'If you didn\'t get the email, try checking your spam folder or search for emails from %4$s.', 'check-spam-folder-message' ) ); + + $notice_title = $this->get_text_inline( 'Thanks for upgrading.', 'after-upgrade-thank-you-message' ); } $this->_admin_notices->add_sticky( - sprintf( - $this->get_text_inline( 'You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s.', 'pending-activation-message' ), - '' . $this->get_plugin_name() . '', - '' . $email . '', - ( $is_pending_trial ? - $this->get_text_inline( 'start the trial', 'start-the-trial' ) : - $this->get_text_inline( 'complete the install', 'complete-the-install' ) ) - ), + vsprintf( $formatted_message, $formatted_message_args ), 'activation_pending', - 'Thanks!' + $notice_title ); } @@ -7193,6 +7077,20 @@ function _admin_init_action() { /** * Don't redirect if activating multiple plugins at once (bulk activation). */ + } else if ( + self::is_deactivation_snoozed() && + ( + // Either running the free code base. + ! $this->is_premium() || + // Or if has a free version. + ! $this->is_only_premium() || + // If premium only, don't redirect if license is activated. + ( $this->is_registered() && ! $this->can_use_premium_code() ) + ) + ) { + /** + * Don't redirect if activating during the deactivation snooze period (aka troubleshooting), unless activating a paid product version that the admin didn't enter its license key yet. + */ } else if ( ! $is_migration ) { $this->_redirect_on_activation_hook(); return; @@ -7206,7 +7104,7 @@ function _admin_init_action() { if ( fs_request_is_action( $this->get_unique_affix() . '_skip_activation' ) ) { check_admin_referer( $this->get_unique_affix() . '_skip_activation' ); - $this->skip_connection( null, fs_is_network_admin() ); + $this->skip_connection( fs_is_network_admin() ); fs_redirect( $this->get_after_activation_url( 'after_skip_url' ) ); } @@ -7379,8 +7277,6 @@ function _enqueue_connect_essentials() { fs_enqueue_local_script( 'postmessage', 'nojquery.ba-postmessage.min.js' ); fs_enqueue_local_script( 'fs-postmessage', 'postmessage.js' ); - - fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' ); } /** @@ -7416,14 +7312,14 @@ function _add_connect_pointer_script() { apply_filters( 'optin_pointer_execute', " - optin.pointer('open'); + optin.pointer('open'); - // Tag the opt-in pointer with custom class. - $('.wp-pointer #fs_connect') - .parents('.wp-pointer.wp-pointer-top') - .addClass('fs-opt-in-pointer'); + // Tag the opt-in pointer with custom class. + $('.wp-pointer #fs_connect') + .parents('.wp-pointer.wp-pointer-top') + .addClass('fs-opt-in-pointer'); - ", 'element', 'optin' ) ?> + ", 'element', 'optin' ) ?> } } }); @@ -7439,7 +7335,7 @@ function _add_connect_pointer_script() { * * @return string */ - function current_page_url() { + static function current_page_url() { $url = 'http'; if ( isset( $_SERVER["HTTPS"] ) ) { @@ -7471,7 +7367,7 @@ function _is_plugin_page() { } /* Events - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /** * Delete site install from Database. * @@ -7637,10 +7533,19 @@ private function get_previous_theme_slug() { * @author Leo Fajardo (@leorw) * @since 1.2.2 * - * @return string + * @return bool */ private function can_activate_previous_theme() { - $slug = $this->get_previous_theme_slug(); + return $this->can_activate_theme( $this->get_previous_theme_slug() ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return bool + */ + private function can_activate_theme( $slug ) { if ( false !== $slug && current_user_can( 'switch_themes' ) ) { $theme_instance = wp_get_theme( $slug ); @@ -7737,6 +7642,10 @@ function _activate_plugin_event_hook() { ( current_filter() !== ( 'activate_' . $this->_free_plugin_basename ) ) : $this->is_premium(); + if ( $is_premium_version_activation && $this->is_pending_activation() ) { + $this->clear_pending_activation_mode(); + } + $this->_logger->info( 'Activating ' . ( $is_premium_version_activation ? 'premium' : 'free' ) . ' plugin version.' ); if ( $this->is_plugin() ) { @@ -7803,7 +7712,9 @@ function _activate_plugin_event_hook() { $plugin_version = $this->_storage->is_anonymous_ms['version']; $network = true; } else { - $plugin_version = $this->_storage->is_anonymous['version']; + $plugin_version = isset( $this->_storage->is_anonymous ) ? + $this->_storage->is_anonymous['version'] : + null; $network = false; } @@ -7894,7 +7805,7 @@ function _activate_plugin_event_hook() { $has_api_connectivity = $this->has_api_connectivity( WP_FS__DEV_MODE || $is_premium_version_activation ); if ( ! $this->_anonymous_mode && - $has_api_connectivity && + ( false !== $has_api_connectivity ) && ! $this->_isAutoInstall ) { // Store hint that the plugin was just activated to enable auto-redirection to settings. @@ -7951,7 +7862,7 @@ private function maybe_activate_addon_license() { ); } else { // Activate the license. - $install = $this->get_api_site_scope()->call( + $install = $this->api_site_call( '/', 'put', array( 'license_key' => $this->apply_filters( 'license_key', $license->secret_key ) ) @@ -8531,9 +8442,9 @@ function _deactivate_plugin_hook() { ) ); } } else { - if ( ! $this->has_api_connectivity() ) { + if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) { // Reset connectivity test cache. - unset( $this->_storage->connectivity_test ); + $this->clear_connectivity_info(); $storage_keys_for_removal[] = 'connectivity_test'; } @@ -8610,6 +8521,20 @@ private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = $this->_is_anonymous = $is_anonymous; } + /** + * @author Vova Feldman (@svovaf) + * @since 2.5.1 + * + * @param bool|int $network_or_blog_id + */ + private function unset_anonymous_mode( $network_or_blog_id = 0 ) { + if ( true === $network_or_blog_id ) { + unset( $this->_storage->is_anonymous_ms ); + } else { + $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id ); + } + } + /** * @author Vova Feldman (@svovaf) * @since 2.0.0 @@ -8626,9 +8551,17 @@ private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = * @uses Freemius::is_network_anonymous() to check if the super-admin network skipped. * @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins. */ - function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) { + public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) { $this->_logger->entrance(); + if ( ! $this->_is_network_active ) { + FS_Clone_Manager::instance()->store_blog_install_info( $blog_id ); + return; + } + + $site = null; + $new_blog_id = $blog_id; + if ( $this->is_premium() && $this->is_network_connected() && is_object( $this->_license ) && @@ -8662,9 +8595,13 @@ function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_ } } + $site = $this->_site; + $this->switch_to_blog( $current_blog_id ); - if ( is_object( $this->_site ) ) { + if ( is_object( $site ) ) { + FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site ); + // Already connected (with or without a license), so no need to continue. return; } @@ -8697,6 +8634,8 @@ function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_ false ); + $site = $this->_site; + $this->switch_to_blog( $current_blog_id ); } else { /** @@ -8707,8 +8646,8 @@ function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_ $has_delegated_site = false; $sites = self::get_sites(); - foreach ( $sites as $site ) { - $blog_id = self::get_site_blog_id( $site ); + foreach ( $sites as $wp_site ) { + $blog_id = self::get_site_blog_id( $wp_site ); if ( $this->is_site_delegated_connection( $blog_id ) ) { $has_delegated_site = true; @@ -8722,19 +8661,74 @@ function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_ $this->skip_site_connection( $blog_id ); } } + + /** + * Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site ); + } + + /** + * @author Vova Feldman (@svovaf) + * @since 2.5.0 + * + * @param \WP_Site $new_site + * @param array $args + */ + public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) { + $this->_logger->entrance(); + + $this->_after_new_blog_callback( + $new_site->id, + // Dummy user ID (not in use). + 0, + $new_site->domain, + $new_site->path, + $new_site->network_id, + // Dummy meta, not in use. + array() + ); } /** * @author Vova Feldman (@svovaf) * @since 1.1.3 * - * @param bool|int $network_or_blog_id Since 2.0.0. + * @param bool|int|int[] $network_or_blog_ids Since 2.0.0. */ - private function reset_anonymous_mode( $network_or_blog_id = 0 ) { - if ( true === $network_or_blog_id ) { - unset( $this->_storage->is_anonymous_ms ); - } else { - $this->_storage->remove( 'is_anonymous', true, $network_or_blog_id ); + private function reset_anonymous_mode( $network_or_blog_ids = false ) { + if ( true === $network_or_blog_ids ) { + $this->unset_anonymous_mode( true ); + + if ( fs_is_network_admin() ) { + $this->_is_anonymous = null; + } + + // Rest anonymous mode for all non-delegated sub-sites. + $blog_ids = $this->get_non_delegated_blog_ids(); + } + else + { + if ( false === $network_or_blog_ids ) { + $network_or_blog_ids = 0; + } + + $blog_ids = is_array( $network_or_blog_ids ) ? + $network_or_blog_ids : + array( $network_or_blog_ids ); + + foreach ( $blog_ids as $blog_id ) { + if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) { + $this->_is_anonymous = null; + } + } + } + + foreach ( $blog_ids as $blog_id ) { + $this->unset_anonymous_mode( $blog_id ); } /** @@ -8745,15 +8739,31 @@ private function reset_anonymous_mode( $network_or_blog_id = 0 ) { * @author Leo Fajardo (@leorw) * @since 1.2.2 */ - if ( ! $this->_is_network_active || - 0 === $network_or_blog_id || - get_current_blog_id() == $network_or_blog_id || - ( true === $network_or_blog_id && fs_is_network_admin() ) - ) { + if ( ! $this->_is_network_active ) { $this->_is_anonymous = null; } } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.3 + */ + private function update_license_required_permissions_if_anonymous() { + if ( ! $this->is_anonymous() ) { + return; + } + + $this->reset_anonymous_mode( fs_is_network_admin() ); + + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + 'essentials' => true, + 'events' => true, + 'diagnostic' => false, + 'extensions' => false, + 'site' => false, + ) ); + } + /** * This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or * deleting the account in the network level, the URL of the page to redirect to is correct. @@ -8778,15 +8788,27 @@ private function maybe_set_slug_and_network_menu_exists_flag() { * @since 1.1.7 */ function connect_again() { - if ( ! $this->is_anonymous() ) { + if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) { return; } - $this->reset_anonymous_mode( fs_is_network_admin() ); + if ( $this->is_anonymous() ) { + $this->reset_anonymous_mode( fs_is_network_admin() ); + } + + $activation_url_params = array(); + + if ( $this->is_pending_activation() ) { + $this->clear_pending_activation_mode(); + + if ( fs_request_get_bool( 'require_license' ) ) { + $activation_url_params['require_license'] = true; + } + } $this->maybe_set_slug_and_network_menu_exists_flag(); - fs_redirect( $this->get_activation_url() ); + fs_redirect( $this->get_activation_url( $activation_url_params ) ); } /** @@ -8795,44 +8817,42 @@ function connect_again() { * @author Vova Feldman (@svovaf) * @since 1.1.1 * - * @param array|null $sites Since 2.0.0. Specific sites. - * @param bool $skip_all_network Since 2.0.0. If true, skip connection for all sites. + * @param bool|int|int[] $network_or_blog_ids Since 2.5.1 */ - function skip_connection( $sites = null, $skip_all_network = false ) { + function skip_connection( $network_or_blog_ids = false ) { $this->_logger->entrance(); $this->_admin_notices->remove_sticky( 'connect_account' ); - if ( $skip_all_network ) { + if ( true === $network_or_blog_ids ) { $this->set_anonymous_mode( true, true ); - } - if ( ! $skip_all_network && empty( $sites ) ) { - $this->skip_site_connection(); - } else { - $uids = array(); + if ( fs_is_network_admin() ) { + $this->_is_anonymous = null; + } + + // Rest anonymous mode for all non-delegated sub-sites. + $blog_ids = $this->get_non_delegated_blog_ids(); + } + else + { + if ( false === $network_or_blog_ids ) { + $network_or_blog_ids = 0; + } - if ( $skip_all_network ) { - $this->set_anonymous_mode( true, true ); + $blog_ids = is_array( $network_or_blog_ids ) ? + $network_or_blog_ids : + array( $network_or_blog_ids ); - $sites = self::get_sites(); - foreach ( $sites as $site ) { - $blog_id = self::get_site_blog_id( $site ); - $this->skip_site_connection( $blog_id, false ); - $uids[] = $this->get_anonymous_id( $blog_id ); - } - } else if ( ! empty( $sites ) ) { - foreach ( $sites as $site ) { - $uids[] = $site['uid']; - $this->skip_site_connection( $site['blog_id'], false ); + foreach ( $blog_ids as $blog_id ) { + if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) { + $this->_is_anonymous = null; } } + } - // Send anonymous skip event. - // No user identified info nor any tracking will be sent after the user skips the opt-in. - $this->get_api_plugin_scope()->call( 'skip.json', 'put', array( - 'uids' => $uids, - ) ); + foreach ( $blog_ids as $blog_id ) { + $this->skip_site_connection( $blog_id ); } $this->network_upgrade_mode_completed(); @@ -8847,18 +8867,12 @@ function skip_connection( $sites = null, $skip_all_network = false ) { * @param int|null $blog_id * @param bool $send_skip */ - private function skip_site_connection( $blog_id = null, $send_skip = true ) { + private function skip_site_connection( $blog_id = null ) { $this->_logger->entrance(); $this->_admin_notices->remove_sticky( 'connect_account', $blog_id ); $this->set_anonymous_mode( true, $blog_id ); - - if ( $send_skip ) { - $this->get_api_plugin_scope()->call( 'skip.json', 'put', array( - 'uids' => array( $this->get_anonymous_id( $blog_id ) ), - ) ); - } } /** @@ -9230,7 +9244,7 @@ private function get_themes_data_for_api() { * @param string[] $override * @param bool $include_plugins Since 1.1.8 by default include plugin changes. * @param bool $include_themes Since 1.1.8 by default include plugin changes. - * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, charset, title, and URL). + * @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL). * * @return array */ @@ -9240,7 +9254,10 @@ private function get_install_data_for_api( $include_themes = true, $include_blog_data = true ) { - if ( $this->is_extensions_tracking_allowed() ) { + // Alias. + $permissions = FS_Permission_Manager::instance( $this ); + + if ( $permissions->is_extensions_tracking_allowed() ) { if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) { /** * @since 1.1.8 Also send plugin updates. @@ -9268,21 +9285,23 @@ private function get_install_data_for_api( $versions = $this->get_versions(); - $blog_data = $include_blog_data ? - array( - 'language' => get_bloginfo( 'language' ), - 'charset' => get_bloginfo( 'charset' ), - 'title' => get_bloginfo( 'name' ), - 'url' => get_site_url(), - ) : - array(); + $blog_data = array(); + if ( $include_blog_data ) { + $blog_data['url'] = self::get_unfiltered_site_url(); + + if ( $permissions->is_diagnostic_tracking_allowed() ) { + $blog_data = array_merge( $blog_data, array( + 'language' => self::get_sanitized_language(), + 'title' => get_bloginfo( 'name' ), + ) ); + } + } return array_merge( $versions, $blog_data, array( 'version' => $this->get_plugin_version(), 'is_premium' => $this->is_premium(), // Special params. 'is_active' => true, - 'is_disconnected' => $this->is_tracking_prohibited(), 'is_uninstalled' => false, ), $override ); } @@ -9297,6 +9316,7 @@ private function get_install_data_for_api( * * @param string[] string $override * @param bool $only_diff + * @param bool $is_keepalive * @param bool $include_plugins Since 1.1.8 by default include plugin changes. * @param bool $include_themes Since 1.1.8 by default include plugin changes. * @@ -9305,6 +9325,7 @@ private function get_install_data_for_api( private function get_installs_data_for_api( array $override, $only_diff = false, + $is_keepalive = false, $include_plugins = true, $include_themes = true ) { @@ -9342,6 +9363,10 @@ private function get_installs_data_for_api( $sites = self::get_sites(); + $subsite_data_for_api_by_install_id = array(); + $install_url_by_install_id = array(); + $subsite_registration_date_by_install_id = array(); + foreach ( $sites as $site ) { $blog_id = self::get_site_blog_id( $site ); @@ -9353,19 +9378,64 @@ private function get_installs_data_for_api( continue; } - if ( ! $this->is_premium() && $install->is_tracking_prohibited() ) { + if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) { // Don't send updates regarding opted-out installs. continue; } - $install_data = $this->get_site_info( $site ); + $install_data = $this->get_site_info( $site, true ); + + if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) { + continue; + } + + $uid = $install_data['uid']; + $url = $install_data['url']; + $registration_date = $install_data['registration_date']; + + if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) { + $clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ]; + $clone_install_url = $install_url_by_install_id[ $install->id ]; + $clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ]; + + $skip = false; + + if ( + ! empty( $install_data['registration_date'] ) && + ! empty( $clone_subsite_registration_date ) + ) { + /** + * If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + */ + $skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) ); + } else if ( + /** + * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) || + fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) ) + ) { + $skip = true; + } - $uid = $install_data['uid']; + if ( $skip ) { + // Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on. + FS_Clone_Manager::instance()->store_blog_install_info( $blog_id ); + continue; + } + } unset( $install_data['blog_id'] ); unset( $install_data['uid'] ); + unset( $install_data['url'] ); + unset( $install_data['registration_date'] ); - $install_data['is_disconnected'] = $install->is_disconnected; $install_data['is_active'] = $this->is_active_for_site( $blog_id ); $install_data['is_uninstalled'] = $install->is_uninstalled; @@ -9388,18 +9458,26 @@ private function get_installs_data_for_api( $is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff; } - if ( ! empty( $install_data ) || $is_common_diff ) { + if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) { // Add install ID and site unique ID. $install_data['id'] = $install->id; $install_data['uid'] = $uid; + $install_data['url'] = $url; - $installs_data[] = $install_data; + $subsite_data_for_api_by_install_id[ $install->id ] = $install_data; + $install_url_by_install_id[ $install->id ] = $install->url; + $subsite_registration_date_by_install_id[ $install->id ] = $registration_date; } } } restore_current_blog(); + $installs_data = array_merge( + $installs_data, + array_values( $subsite_data_for_api_by_install_id ) + ); + if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) { if ( ! $only_diff ) { $installs_data[] = $common; @@ -9437,8 +9515,12 @@ private function get_install_diff_for_api( $site, $install, $override = array() if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) && $install->{$p} != $v ) { - $install->{$p} = $v; - $diff[ $p ] = $v; + $val = self::get_api_sanitized_property( $p, $v ); + + if ( $install->{$p} != $val ) { + $install->{$p} = $val; + $diff[ $p ] = $val; + } } } else { $special[ $p ] = $v; @@ -9460,7 +9542,79 @@ private function get_install_diff_for_api( $site, $install, $override = array() $diff = array_merge( $diff, $special ); } - return $diff; + return $diff; + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + */ + private function send_pending_clone_update_once() { + $this->_logger->entrance(); + + if ( ! empty( $this->_storage->clone_id ) ) { + return; + } + + $install_clone = $this->get_api_site_scope()->call( + '/clones', + 'post', + array( 'site_url' => self::get_unfiltered_site_url() ) + ); + + if ( $this->is_api_result_entity( $install_clone ) ) { + $this->_storage->clone_id = $install_clone->id; + } + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + * + * @param string $resolution_type + * @param FS_Site $clone_context_install + */ + function send_clone_resolution_update( $resolution_type, $clone_context_install ) { + $this->_logger->entrance(); + + if ( empty( $this->_storage->clone_id ) ) { + return; + } + + $new_install_id = null; + $current_site = null; + + $flush = false; + + /** + * If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity. + */ + if ( $clone_context_install->id != $this->_site->id ) { + $new_install_id = $this->_site->id; + $current_site = $this->_site; + $this->_site = $clone_context_install; + + $flush = true; + } + + $this->get_api_site_scope( $flush )->call( + "/clones/{$this->_storage->clone_id}", + 'put', + array( + 'resolution' => $resolution_type, + 'new_install_id' => $new_install_id, + ) + ); + + if ( is_object( $current_site ) ) { + /** + * Ensure that the install scope entity is updated back to the previous install entity. + */ + $this->_site = $current_site; + + // Restore the previous install scope entity of the API. + $this->get_api_site_scope( true ); + } } /** @@ -9471,10 +9625,11 @@ private function get_install_diff_for_api( $site, $install, $override = array() * * @param string[] string $override * @param bool $flush + * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared. * * @return false|object|string */ - private function send_install_update( $override = array(), $flush = false ) { + private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) { $this->_logger->entrance(); $check_properties = $this->get_install_data_for_api( $override ); @@ -9485,7 +9640,6 @@ private function send_install_update( $override = array(), $flush = false ) { $params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override ); } - $keepalive_only_update = false; if ( empty( $params ) ) { $keepalive_only_update = $this->should_send_keepalive_update(); @@ -9500,10 +9654,9 @@ private function send_install_update( $override = array(), $flush = false ) { } } - if ( ! $keepalive_only_update ) { + if ( $is_two_way_sync ) { /** - * Do not update the last install sync timestamp after a keepalive-only call since there were no actual - * updates sent. + * Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call. * * @author Leo Fajardo (@leorw) * @since 2.2.3 @@ -9519,11 +9672,11 @@ private function send_install_update( $override = array(), $flush = false ) { $this->set_keepalive_timestamp(); // Send updated values to FS. - $site = $this->get_api_site_scope()->call( '/', 'put', $params ); + $site = $this->api_site_call( '/', 'put', $params, true ); - if ( ! $keepalive_only_update && $this->is_api_result_entity( $site ) ) { + if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) { /** - * Do not clear scheduled sync after a keepalive-only call since there were no actual updates sent. + * Clear scheduled install sync after a two-way sync call. * * @author Leo Fajardo (@leorw) * @since 2.2.3 @@ -9545,37 +9698,29 @@ private function send_install_update( $override = array(), $flush = false ) { * * @param string[] string $override * @param bool $flush + * @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared. * * @return false|object|string */ - private function send_installs_update( $override = array(), $flush = false ) { + private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) { $this->_logger->entrance(); - $installs_data = $this->get_installs_data_for_api( $override, ! $flush ); + /** + * Pass `true` to use the network level storage since the update is for many installs. + * + * @author Leo Fajardo (@leorw) + * @since 2.2.3 + */ + $should_send_keepalive = $this->should_send_keepalive_update( true ); - $keepalive_only_update = false; - if ( empty( $installs_data ) ) { - /** - * Pass `true` to use the network level storage since the update is for many installs. - * - * @author Leo Fajardo (@leorw) - * @since 2.2.3 - */ - $keepalive_only_update = $this->should_send_keepalive_update( true ); + $installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive ); - if ( ! $keepalive_only_update ) { - /** - * There are no updates to send including keepalive. - * - * @author Leo Fajardo (@leorw) - * @since 2.2.3 - */ - return false; - } + if ( empty( $installs_data ) ) { + return false; } - if ( ! $keepalive_only_update ) { - // Update last install sync timestamp if there were actual updates sent (i.e., not a keepalive-only call). + if ( $is_two_way_sync ) { + // Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call. $this->set_cron_execution_timestamp( 'install_sync' ); } @@ -9590,8 +9735,8 @@ private function send_installs_update( $override = array(), $flush = false ) { // Send updated values to FS. $result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data ); - if ( ! $keepalive_only_update && $this->is_api_result_object( $result, 'installs' ) ) { - // I successfully sent installs update (there was an actual update sent and it's not just a keepalive-only call), clear scheduled sync if exist. + if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) { + // I successfully sent a two-way installs update, clear the scheduled install sync if it exists. $this->clear_install_sync_cron(); } @@ -9641,10 +9786,10 @@ private function maybe_sync_install_user() { * @param string[] string $override * @param bool $flush */ - private function sync_install( $override = array(), $flush = false ) { + function sync_install( $override = array(), $flush = false ) { $this->_logger->entrance(); - $site = $this->send_install_update( $override, $flush ); + $site = $this->send_install_update( $override, $flush, true ); if ( false === $site ) { // No sync required. @@ -9673,7 +9818,7 @@ private function sync_install( $override = array(), $flush = false ) { private function sync_installs( $override = array(), $flush = false ) { $this->_logger->entrance(); - $result = $this->send_installs_update( $override, $flush ); + $result = $this->send_installs_update( $override, $flush, true ); if ( false === $result ) { // No sync required. @@ -9828,8 +9973,8 @@ function _uninstall_plugin_event( $check_user = true ) { // Send uninstall event. $this->send_installs_update( $params ); } else { - // Send uninstall event. - $this->send_install_update( $params ); + // Send uninstall event and handle the result. + $this->sync_install( $params ); } } @@ -9926,7 +10071,17 @@ public static function _uninstall_plugin_hook() { return; } - $fs->_uninstall_plugin_event(); + if ( + ! $fs->is_clone() && + /** + * If there's a context install, run this method only when there's also a context user (e.g., when cloning a subsite of a multisite network into a single-site installation, it's possible for an install to be associated with a non-existing user entity; we want Freemius to be off in this case, while we are trying to recover the user). + * + * @author Leo Fajardo + */ + ( ! is_object( $fs->_site ) || $fs->is_registered() ) + ) { + $fs->_uninstall_plugin_event(); + } $fs->do_action( 'after_uninstall' ); } @@ -10037,7 +10192,7 @@ function get_slug() { * @return string */ function get_premium_slug() { - return is_object( $this->_plugin ) ? + return ( is_object( $this->_plugin ) && ! empty( $this->_plugin->premium_slug ) ) ? $this->_plugin->premium_slug : "{$this->_slug}-premium"; } @@ -10090,6 +10245,28 @@ function get_bundle_public_key() { null; } + /** + * Get whether the SDK has been initiated in the context of a Bundle. + * + * This will return true, if `bundle_id` is present in the SDK init parameters. + * + * ```php + * $my_fs = fs_dynamic_init( array( + * // ... + * 'bundle_id' => 'XXXX', // Will return true since we have bundle id. + * 'bundle_public_key' => 'pk_XXXX', + * ) ); + * ``` + * + * @author Swashata Ghosh (@swashata) + * @since 2.5.0 + * + * @return bool True if we are running in bundle context, false otherwise. + */ + private function has_bundle_context() { + return ! is_null( $this->get_bundle_id() ); + } + /** * @author Vova Feldman (@svovaf) * @since 1.2.1.5 @@ -10121,7 +10298,20 @@ function get_parent_id() { function get_usage_tracking_terms_url() { return $this->apply_filters( 'usage_tracking_terms_url', - "https://freemius.com/wordpress/usage-tracking/{$this->_plugin->id}/{$this->_slug}/" + "https://freemius.com/product/opt-in/{$this->_plugin->id}/{$this->_slug}/" + ); + } + + /** + * @todo (For LiteSDK) We can refactor this and other related functions giving links to several landing pages on freemius.com to come from a separate class like `FS_Terms_Pages`. This would get a `FS_WP_Hook` (hypothetical) instance as a dependency and use it to hook into the `license_activation_terms_url` or related filters. The entry level instance from `ms_fs()` would hold a public read-only variable `my_fs()->terms_pages` which would be an instance of `FS_Terms_Pages` and would hold all the links to the terms pages. + * @since 2.5.8 + * + * @return string + */ + function get_license_activation_terms_url() { + return $this->apply_filters( + 'license_activation_terms_url', + "https://freemius.com/product/license-activation/{$this->_plugin->id}/{$this->_slug}/" ); } @@ -10134,7 +10324,7 @@ function get_usage_tracking_terms_url() { function get_eula_url() { return $this->apply_filters( 'eula_url', - "https://freemius.com/terms/{$this->_plugin->id}/{$this->_slug}/" + "https://freemius.com/product/{$this->_plugin->id}/{$this->_slug}/legal/eula/" ); } @@ -10322,7 +10512,7 @@ function get_plugin_folder_name() { #endregion ------------------------------------------------------------------ /* Account - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /** * Find plugin's slug by plugin's basename. @@ -10386,9 +10576,14 @@ static function get_all_users() { */ private static function get_all_sites( $module_type = WP_FS__MODULE_TYPE_PLUGIN, - $blog_id = null + $blog_id = null, + $is_backup = false ) { - $sites = self::get_account_option( 'sites', $module_type, $blog_id ); + $sites = self::get_account_option( + ( $is_backup ? 'prev_' : '' ) . 'sites', + $module_type, + $blog_id + ); if ( ! is_array( $sites ) ) { $sites = array(); @@ -10748,10 +10943,20 @@ private static function get_all_account_addons() { * * @author Vova Feldman (@svovaf) * @since 1.0.1 + * + * @param bool $ignore_anonymous_state Since 2.5.1 + * * @return bool */ - function is_registered() { - return is_object( $this->_user ); + function is_registered( $ignore_anonymous_state = false ) { + return ( + is_object( $this->_user ) && + ( + $this->is_premium() || + $ignore_anonymous_state || + ! $this->is_anonymous() + ) + ); } /** @@ -10762,8 +10967,34 @@ function is_registered() { * * @return bool */ - function is_tracking_allowed() { - return ( is_object( $this->_site ) && $this->_site->is_tracking_allowed() ); + function is_tracking_allowed( $blog_id = null, $install = null ) { + if ( is_null( $install ) ) { + $install = is_null( $blog_id ) ? + $this->_site : + $this->get_install_by_blog_id( $blog_id ); + } + + return ( + is_object( $install ) && + FS_Permission_Manager::instance( $this )->is_homepage_url_tracking_allowed( $blog_id ) + ); + } + + /** + * Returns TRUE if the user never opted-in or manually opted-out. + * + * @author Vova Feldman (@svovaf) + * @since 1.2.1.5 + * + * @param int|null $blog_id + * + * @return bool + */ + function is_tracking_prohibited( $blog_id = null ) { + return ( + ! $this->is_registered( true ) || + ! $this->is_tracking_allowed( $blog_id ) + ); } /** @@ -10808,6 +11039,52 @@ function get_site() { return $this->_site; } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function store_site( $site ) { + $this->_site = $site; + $this->_store_site( true ); + } + + /** + * Deletes the current install with an option to back it up in case restoration will be needed (e.g., if the automatic clone resolution attempt fails). + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function delete_current_install( $back_up ) { + // Back up and delete the unique ID. + if ( $back_up ) { + self::$_accounts->set_option( 'prev_unique_id', $this->get_anonymous_id() ); + } + + self::$_accounts->set_option( 'unique_id', null ); + + if ( $back_up ) { + // Back up the install before deleting it so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails). + $this->back_up_site(); + } + + $this->_delete_site(); + $this->_site = null; + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function restore_backup_site() { + self::$_accounts->set_option( + 'unique_id', + self::$_accounts->get_option( 'prev_unique_id' ) + ); + + $sites = self::get_all_sites( $this->_module_type, null, true ); + $this->store_site( clone $sites[ $this->_slug ] ); + } + /** * Get plugin add-ons. * @@ -11252,7 +11529,7 @@ function get_plan() { function is_trial() { $this->_logger->entrance(); - if ( ! $this->is_registered() || ! is_object( $this->_site ) ) { + if ( ! $this->is_registered( true ) || ! is_object( $this->_site ) ) { return false; } @@ -11366,7 +11643,7 @@ function get_trial_plan() { function is_paying() { $this->_logger->entrance(); - if ( ! $this->is_registered() ) { + if ( ! $this->is_registered( true ) ) { return false; } @@ -12089,7 +12366,7 @@ private function can_activate_license_on_network( FS_Plugin_License $license ) { } else { $url = is_object( $site ) ? $site->siteurl : - get_site_url( $blog_id ); + self::get_unfiltered_site_url( $blog_id ); $disconnected_site_ids[] = $blog_id; } @@ -12462,7 +12739,21 @@ function is_whitelabeled( $ignore_data_debug_mode = false, $blog_id = null ) { } else if ( $is_whitelabeled_flag ) { $is_whitelabeled = true; } else { - $addon_ids = $this->get_updated_account_addons(); + if ( $this->is_registered() || $this->is_premium() ) { + $addon_ids = $this->get_updated_account_addons(); + } else { + $addons = self::get_all_addons(); + + $plugin_addons = isset( $addons[ $this->_plugin->id ] ) ? + $addons[ $this->_plugin->id ] : + array(); + + $addon_ids = array(); + foreach ( $plugin_addons as $addon ) { + $addon_ids[] = $addon->id; + } + } + $installed_addons = $this->get_installed_addons(); foreach ( $installed_addons as $fs_addon ) { $addon_ids[] = $fs_addon->get_id(); @@ -12891,6 +13182,75 @@ function _add_license_activation_dialog_box() { fs_require_template( 'forms/resend-key.php', $vars ); } + /** + * Displays an email address update dialog box when the user clicks on the email address "Edit" button on the "Account" page. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _add_email_address_update_dialog_box() { + $vars = array( 'id' => $this->_module_id ); + + fs_require_template( 'forms/email-address-update.php', $vars ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _add_email_address_update_option() { + if ( ! $this->should_handle_user_change() ) { + return; + } + + // Add email address update AJAX handler. + $this->add_ajax_action( 'update_email_address', array( &$this, '_email_address_update_ajax_handler' ) ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _email_address_update_ajax_handler() { + $this->check_ajax_referer( 'update_email_address' ); + + $new_email_address = fs_request_get( 'email_address' ); + $transfer_type = fs_request_get( 'transfer_type' ); + + $result = $this->update_email( $new_email_address ); + + if ( ! FS_Api::is_api_error( $result ) ) { + self::shoot_ajax_success(); + } + + $error = ''; + + if ( FS_Api::is_api_error_object( $result ) ) { + switch ( $result->error->code ) { + case 'user_exist': + case 'account_verification_required': + $error = array( + 'code' => 'change_ownership', + 'url' => $this->get_account_url( 'change_owner', array( + 'state' => 'init', + 'candidate_email' => $new_email_address, + 'transfer_type' => $transfer_type, + ) ), + ); + + break; + } + } + + if ( empty( $error ) ) { + $error = is_object( $result ) ? + var_export( $result->error, true ) : + $result; + } + + self::shoot_ajax_failure( $error ); + } + /** * Returns a collection of IDs of installs that are associated with the context product and its add-ons, and activated with foreign licenses. * @@ -13098,10 +13458,15 @@ function _add_license_activation() { ( $is_network_admin && $this->is_network_active() && ! $this->is_network_delegated_connection() ) || ( ! $is_network_admin && ( ! $this->is_network_active() || $this->is_delegated_connection() ) ) ) { - /** - * @since 1.2.0 Add license action link only on plugins page. - */ - $this->_add_license_action_link(); + if ( + $this->is_premium() || + ( $this->has_paid_plan() && ! $this->has_premium_version() ) + ) { + /** + * @since 1.2.0 Add license action link only on plugins page. + */ + $this->_add_license_action_link(); + } } } @@ -13273,7 +13638,7 @@ function _set_beta_mode_ajax_handler() { self::shoot_ajax_failure(); } - $site = $this->get_api_site_scope()->call( + $site = $this->api_site_call( '', 'put', array( @@ -13309,7 +13674,7 @@ function _activate_license_ajax_action() { $this->check_ajax_referer( 'activate_license' ); - $license_key = trim( fs_request_get( 'license_key' ) ); + $license_key = trim( fs_request_get_raw( 'license_key' ) ); if ( empty( $license_key ) ) { exit; @@ -13326,7 +13691,8 @@ function _activate_license_ajax_action() { fs_request_get( 'blog_id', null ), fs_request_get( 'module_id', null, 'post' ), fs_request_get( 'user_id', null ), - fs_request_get_bool( 'is_extensions_tracking_allowed', null ) + fs_request_get_bool( 'is_extensions_tracking_allowed', null ), + fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ) ); if ( @@ -13571,6 +13937,9 @@ function should_use_external_pricing() { * @param null|int $blog_id * @param null|number $plugin_id * @param null|number $license_owner_id + * @param bool|null $is_extensions_tracking_allowed + * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint. + * * * @return array { * @var bool $success @@ -13585,7 +13954,8 @@ private function activate_license( $blog_id = null, $plugin_id = null, $license_owner_id = null, - $is_extensions_tracking_allowed = null + $is_extensions_tracking_allowed = null, + $is_diagnostic_tracking_allowed = null ) { $this->_logger->entrance(); @@ -13605,7 +13975,10 @@ private function activate_license( $this : $this->get_addon_instance( $plugin_id ); - $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed ); + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed, + FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed, + ) ); $error = false; $next_page = false; @@ -13635,6 +14008,8 @@ private function activate_license( } if ( is_object( $user ) ) { + $result = true; + if ( $is_network_activation_or_migration && ! $has_valid_blog_id ) { // If no specific blog ID was provided, activate the license for all sites in the network. $blog_2_install_map = array(); @@ -13656,22 +14031,10 @@ private function activate_license( if ( ! empty( $blog_2_install_map ) ) { $result = $fs->activate_license_on_many_installs( $user, $license_key, $blog_2_install_map ); - - if ( true !== $result ) { - $error = FS_Api::is_api_error_object( $result ) ? - $result->error->message : - var_export( $result, true ); - } } - if ( empty( $error ) && ! empty( $site_ids ) ) { + if ( true === $result && ! empty( $site_ids ) ) { $result = $fs->activate_license_on_many_sites( $user, $license_key, $site_ids ); - - if ( true !== $result ) { - $error = FS_Api::is_api_error_object( $result ) ? - $result->error->message : - var_export( $result, true ); - } } } else { if ( $fs->is_registered() ) { @@ -13697,13 +14060,11 @@ private function activate_license( $api = $fs->get_api_site_scope(); - $install = $api->call( $fs->add_show_pending( '/' ), 'put', $params ); + $result = $api->call( $fs->add_show_pending( '/' ), 'put', $params ); + + if ( ! FS_Api::is_api_error( $result ) ) { + $install = $result; - if ( FS_Api::is_api_error( $install ) ) { - $error = FS_Api::is_api_error_object( $install ) ? - $install->error->message : - var_export( $install->error, true ); - } else { $fs->reconnect_locally( $has_valid_blog_id ); if ( @@ -13716,16 +14077,18 @@ private function activate_license( } } else /* ( $fs->is_addon() && $fs->get_parent_instance()->is_registered() ) */ { $result = $fs->activate_license_on_site( $user, $license_key ); - - if ( true !== $result ) { - $error = FS_Api::is_api_error_object( $result ) ? - $result->error->message : - var_export( $result, true ); - } } } - if ( empty( $error ) ) { + if ( true !== $result && ! FS_Api::is_api_result_entity( $result ) ) { + if ( FS_Api::is_blocked( $result ) ) { + $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result ); + } + + $error = FS_Api::is_api_error_object( $result ) ? + $result->error->message : + var_export( $result, true ); + } else { $fs->network_upgrade_mode_completed(); $fs->_user = $user; @@ -13770,8 +14133,8 @@ private function activate_license( } } - $all_sites = self::get_sites(); - $pending_sites = array(); + $all_sites = self::get_sites(); + $pending_blog_ids = array(); /** * Check if there are any sites that are not connected, skipped, nor delegated. For every site that falls into that category, if the product is freemium, skip the connection. If the product is premium only, delegate the connection to the site administrator. @@ -13801,14 +14164,14 @@ private function activate_license( continue; } - $pending_sites[] = self::get_site_info( $site ); + $pending_blog_ids[] = $blog_id; } - if ( ! empty( $pending_sites ) ) { + if ( ! empty( $pending_blog_ids ) ) { if ( $fs->is_freemium() && $fs->is_enable_anonymous() ) { - $fs->skip_connection( $pending_sites ); + $fs->skip_connection( $pending_blog_ids ); } else { - $fs->delegate_connection( $pending_sites ); + $fs->delegate_connection( $pending_blog_ids ); } } } @@ -13886,7 +14249,7 @@ private function get_parent_and_addons_installs_info() { $addon_info = $fs->_get_addon_info( $addon_id, $is_installed ); - if ( ! $addon_info['is_connected'] ) { + if ( ! isset( $addon_info['is_connected'] ) || ! $addon_info['is_connected'] ) { // Add-on is not associated with an install entity. continue; } @@ -13944,11 +14307,11 @@ function _network_activate_ajax_action() { $this->delegate_connection(); } else { if ( ! empty( $sites_by_action['delegate'] ) ) { - $this->delegate_connection( $sites_by_action['delegate'] ); + $this->delegate_connection( self::get_sites_blog_ids( $sites_by_action['delegate'] ) ); } if ( ! empty( $sites_by_action['skip'] ) ) { - $this->skip_connection( $sites_by_action['skip'] ); + $this->skip_connection( self::get_sites_blog_ids( $sites_by_action['skip'] ) ); } if ( empty( $sites_by_action['allow'] ) ) { @@ -14266,15 +14629,37 @@ function has_affiliate_program() { return $this->_plugin->has_affiliate_program(); } + /** + * Get Plugin ID under which we will track affiliate application. + * + * This could either be the Bundle ID or the main plugin ID. + * + * @return number Bundle ID if developer has provided one, else the main plugin ID. + */ + private function get_plugin_id_for_affiliate_terms() { + return $this->has_bundle_context() ? + $this->get_bundle_id() : + $this->_plugin->id; + } + /** * @author Leo Fajardo (@leorw) * @since 1.2.4 */ private function fetch_affiliate_terms() { if ( ! is_object( $this->plugin_affiliate_terms ) ) { - $plugins_api = $this->get_api_plugin_scope(); + /** + * In case we have a bundle set in SDK configuration, we would like to use that for affiliates, not the main plugin. + */ + $plugins_api = $this->has_bundle_context() ? + $this->get_api_bundle_scope() : + $this->get_api_plugin_scope(); + $affiliate_terms = $plugins_api->get( '/aff.json?type=affiliation', false ); + /** + * At this point, we intentionally don't fallback to the main plugin, because the developer has chosen to use bundle. So it makes sense the affiliate program should be in context to the bundle too. + */ if ( ! $this->is_api_result_entity( $affiliate_terms ) ) { return; } @@ -14292,8 +14677,10 @@ private function fetch_affiliate_and_custom_terms() { $application_data = $this->_storage->affiliate_application_data; $flush = ( ! isset( $application_data['status'] ) || 'pending' === $application_data['status'] ); + $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms(); + $users_api = $this->get_api_user_scope(); - $result = $users_api->get( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush ); + $result = $users_api->get( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json", $flush ); if ( $this->is_api_result_object( $result, 'affiliates' ) ) { if ( ! empty( $result->affiliates ) ) { $affiliate = new FS_Affiliate( $result->affiliates[0] ); @@ -14393,15 +14780,17 @@ function _submit_affiliate_application() { var_export( $next_page, true ) ); } else if ( $this->is_pending_activation() ) { - self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation.', 'account-is-pending-activation' ) ); + self::shoot_ajax_failure( $this->get_text_inline( 'Account is pending activation. Please check your email and click the link to activate your account and then submit the affiliate form again.', 'account-is-pending-activation' ) ); } } $this->fetch_affiliate_terms(); + $plugin_id_for_affiliate = $this->get_plugin_id_for_affiliate_terms(); + $api = $this->get_api_user_scope(); $result = $api->call( - ( "/plugins/{$this->_plugin->id}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ), + ( "/plugins/{$plugin_id_for_affiliate}/aff/{$this->plugin_affiliate_terms->id}/affiliates.json" ), 'post', $affiliate ); @@ -14522,7 +14911,11 @@ function version_upgrade_checkout_link( $new_version ) { return sprintf( $this->get_text_inline( '%s to access version %s security & feature updates, and support.', 'x-for-updates-and-support' ), - sprintf( '%s', $url, $purchase_license_text ), + sprintf( + '%s', + $this->apply_filters( 'update_notice_checkout_url', $url ), + $purchase_license_text + ), $new_version ); } @@ -14592,7 +14985,7 @@ function checkout_url( */ $params = array_merge( $params, $extra ); - return $this->_get_admin_page_url( 'pricing', $params, $network ); + return $this->apply_filters( 'checkout_url', $this->_get_admin_page_url( 'pricing', $params, $network ) ); } /** @@ -14863,6 +15256,16 @@ static function is_cron() { return ( defined( 'DOING_CRON' ) && DOING_CRON ); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return bool + */ + static function is_admin_post() { + return ( 'admin-post.php' === self::get_current_page() ); + } + /** * Check if a real user is visiting the admin dashboard. * @@ -14876,7 +15279,7 @@ function is_user_in_admin() { is_admin() && ! self::is_ajax() && ! self::is_cron() && - ( 'admin-post.php' !== self::get_current_page() ) + ! self::is_admin_post() ); } @@ -15028,20 +15431,20 @@ function is_network_active() { * @author Leo Fajardo (@leorw) * @since 2.0.0 * - * @param array|null $sites + * @param bool|int[] $all_or_blog_ids */ - private function delegate_connection( $sites = null ) { + private function delegate_connection( $all_or_blog_ids = true ) { $this->_logger->entrance(); $this->_admin_notices->remove_sticky( 'connect_account' ); - if ( is_null( $sites ) ) { + if ( true === $all_or_blog_ids ) { // All sites delegation. - $this->_storage->store( 'is_delegated_connection', true, true, true ); + $this->_storage->store( 'is_delegated_connection', true, true ); } else { // Specified sites delegation. - foreach ( $sites as $site ) { - $this->delegate_site_connection( $site['blog_id'] ); + foreach ( $all_or_blog_ids as $blog_id ) { + $this->delegate_site_connection( $blog_id ); } } @@ -15057,7 +15460,7 @@ private function delegate_connection( $sites = null ) { * @param int $blog_id */ private function delegate_site_connection( $blog_id ) { - $this->_storage->store( 'is_delegated_connection', true, $blog_id, true ); + $this->_storage->store( 'is_delegated_connection', true, $blog_id ); } /** @@ -15097,7 +15500,7 @@ function is_site_delegated_connection( $blog_id = 0 ) { } /** - * Check if delegated the connection. When running within the the network admin, + * Check if delegated the connection. When running within the network admin, * and haven't specified the blog ID, checks if network level delegated. If running * within a site admin or specified a blog ID, check if delegated the connection for * the current context site. @@ -15157,12 +15560,17 @@ function is_active_for_site( $blog_id ) { } /** + * @todo Implement pagination when accessing the subsites collection. + * * @author Leo Fajardo (@leorw) * @since 2.0.0 * + * @param int $limit Default to 1,000 + * @param int $offset Default to 0 + * * @return array Active & public sites collection. */ - static function get_sites() { + static function get_sites( $limit = 1000, $offset = 0 ) { if ( ! is_multisite() ) { return array(); } @@ -15184,27 +15592,11 @@ static function get_sites() { 'mature' => 0, 'spam' => 0, 'deleted' => 0, + 'number' => $limit, + 'offset' => $offset, ); - if ( function_exists( 'get_sites' ) ) { - // For WP 4.6 and above. - return get_sites( $args ); - } else if ( function_exists( 'wp_' . 'get_sites' ) ) { - // For WP 3.7 to WP 4.5. - /** - * This is a hack suggested previously proposed by the TRT. Our SDK is compliant with older WP versions and we'd like to keep it that way. - * - * @todo Remove this hack once this false-positive error is removed from the Theme Sniffer. - * - * @since 2.3.3 - * @author Vova Feldman (@svovaf) - */ - $fn = 'wp_' . 'get_sites'; - return $fn( $args ); - } else { - // For WP 3.6 and below. - return get_blog_list( 0, 'all' ); - } + return get_sites( $args ); } /** @@ -15253,7 +15645,7 @@ private function get_address_to_blog_map() { $address_to_blog_map = array(); foreach ( $sites as $site ) { $blog_id = self::get_site_blog_id( $site ); - $address = trailingslashit( fs_strip_url_protocol( get_site_url( $blog_id ) ) ); + $address = self::get_unfiltered_site_url( $blog_id, true, true ); $address_to_blog_map[ $address ] = $blog_id; } @@ -15289,6 +15681,42 @@ function get_blog_install_map() { return $install_map; } + /** + * @author Vova Feldman (@svovaf) + * @since 2.5.1 + * + * @param bool|null $is_delegated When `true`, returns only connection delegated blog IDs. When `false`, only non-delegated blog IDs. + * + * @return int[] + */ + private function get_blog_ids( $is_delegated = null ) { + $blog_ids = array(); + + $sites = self::get_sites(); + foreach ( $sites as $site ) { + $blog_id = self::get_site_blog_id( $site ); + + if ( + is_null( $is_delegated ) || + $is_delegated === $this->is_site_delegated_connection( $blog_id ) + ) { + $blog_ids[] = $blog_id; + } + } + + return $blog_ids; + } + + /** + * @author Vova Feldman (@svovaf) + * @since 2.5.1 + * + * @return int[] + */ + private function get_non_delegated_blog_ids() { + return $this->get_blog_ids( false ); + } + /** * Gets a map of module IDs that the given user has opted-in to. * @@ -15373,11 +15801,16 @@ function find_first_install() { * * @param int $blog_id * @param FS_Site $install + * @param bool $flush * * @return bool Since 2.3.1 returns if a switch was made. */ - function switch_to_blog( $blog_id, FS_Site $install = null ) { - if ( ! is_numeric( $blog_id ) || $blog_id == $this->_context_is_network_or_blog_id ) { + function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) { + if ( ! is_numeric( $blog_id ) ) { + return false; + } + + if ( ! $flush && $blog_id == $this->_context_is_network_or_blog_id ) { return false; } @@ -15441,7 +15874,7 @@ function switch_to_blog( $blog_id, FS_Site $install = null ) { unset( $this->_site_api ); unset( $this->_user_api ); - return false; + return true; } /** @@ -15462,12 +15895,29 @@ function restore_current_blog() { * * @return int */ - static function get_site_blog_id( &$site ) { - return ( $site instanceof WP_Site ) ? - $site->blog_id : - ( is_object( $site ) && isset( $site->userblog_id ) ? - $site->userblog_id : - $site['blog_id'] ); + static function get_site_blog_id( &$site ) { + return ( $site instanceof WP_Site ) ? + $site->blog_id : + ( is_object( $site ) && isset( $site->userblog_id ) ? + $site->userblog_id : + $site['blog_id'] ); + } + + /** + * @author Vova Feldman (@svovaf) + * @since 2.5.1 + * + * @param WP_Site[]|array[] $sites + * + * @return int[] + */ + static function get_sites_blog_ids( $sites ) { + $blog_ids = array(); + foreach ( $sites as $site ) { + $blog_ids[] = self::get_site_blog_id( $site ); + } + + return $blog_ids; } /** @@ -15475,16 +15925,19 @@ static function get_site_blog_id( &$site ) { * @since 2.0.0 * * @param array|WP_Site|null $site + * @param bool $load_registration Since 2.5.1 When set to `true` the method will attempt to return the subsite's registration date, regardless of the `$site` type and value. In most calls, the registration date will be returned anyway, even when the value is `false`. This param is purely for performance optimization. * * @return array */ - function get_site_info( $site = null ) { + function get_site_info( $site = null, $load_registration = false ) { $this->_logger->entrance(); $switched = false; + $registration_date = null; + if ( is_null( $site ) ) { - $url = get_site_url(); + $url = self::get_unfiltered_site_url(); $name = get_bloginfo( 'name' ); $blog_id = null; } else { @@ -15496,26 +15949,44 @@ function get_site_info( $site = null ) { } if ( $site instanceof WP_Site ) { - $url = $site->siteurl; - $name = $site->blogname; + $url = $site->siteurl; + $name = $site->blogname; + $registration_date = $site->registered; } else { - $url = get_site_url( $blog_id ); + $url = self::get_unfiltered_site_url( $blog_id ); $name = get_bloginfo( 'name' ); } } + if ( empty( $registration_date ) && $load_registration ) { + $blog_details = get_blog_details( $blog_id, false ); + + if ( is_object( $blog_details ) && isset( $blog_details->registered ) ) { + $registration_date = $blog_details->registered; + } + } + $info = array( - 'uid' => $this->get_anonymous_id( $blog_id ), - 'url' => $url, - 'title' => $name, - 'language' => get_bloginfo( 'language' ), - 'charset' => get_bloginfo( 'charset' ), + 'uid' => $this->get_anonymous_id( $blog_id ), + 'url' => $url, ); + // Add these diagnostic information only if user allowed to track. + if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) { + $info = array_merge( $info, array( + 'title' => $name, + 'language' => self::get_sanitized_language(), + ) ); + } + if ( is_numeric( $blog_id ) ) { $info['blog_id'] = $blog_id; } + if ( ! empty( $registration_date ) ) { + $info[ 'registration_date' ] = $registration_date; + } + if ( $switched ) { restore_current_blog(); } @@ -15738,6 +16209,10 @@ private function update_multisite_data_after_site_deactivation( $context_blog_id } } + if ( ! $this->is_registered() ) { + return; + } + if ( $this->is_sync_cron_scheduled() && $context_blog_id == $this->get_sync_cron_blog_id() ) { @@ -15771,6 +16246,10 @@ public function _after_site_deactivated_callback( $context_blog_id = 0 ) { $this->update_multisite_data_after_site_deactivation( $context_blog_id ); + if ( ! $this->is_registered() ) { + return; + } + $current_blog_id = get_current_blog_id(); $this->switch_to_blog( $context_blog_id ); @@ -15804,6 +16283,10 @@ public function _after_site_deleted_callback( $context_blog_id = 0, $drop = fals $this->update_multisite_data_after_site_deactivation( $context_blog_id ); + if ( ! $this->is_registered() ) { + return; + } + $current_blog_id = get_current_blog_id(); $this->switch_to_blog( $context_blog_id ); @@ -15821,6 +16304,20 @@ public function _after_site_deleted_callback( $context_blog_id = 0, $drop = fals $this->switch_to_blog( $current_blog_id ); } + /** + * Executed after site deletion, called from wp_delete_site + * + * @author Dario Curvino (@dudo) + * @since 2.5.0 + * + * @param WP_Site $old_site + */ + public function _after_wpsite_deleted_callback( WP_Site $old_site ) { + $this->_logger->entrance(); + + $this->_after_site_deleted_callback( $old_site->blog_id, true ); + } + /** * Executed after site re-activation. * @@ -15935,9 +16432,17 @@ function is_theme_settings_page() { * @return bool */ function is_product_settings_page() { + $page = fs_request_get( 'page', '', 'get' ); + $menu_slug = $this->_menu->get_slug(); + + if ( $page === $menu_slug ) { + return true; + } + return fs_starts_with( - fs_request_get( 'page', '', 'get' ), - $this->_menu->get_slug() + // e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc. + $page, + ( $menu_slug . '-' ) ); } @@ -16017,10 +16522,11 @@ function get_account_tab_url( $tab, $action = false, $params = array(), $add_act * * @param bool|string $topic * @param bool|string $message + * @param bool|string $summary Since 2.5.1. * * @return string */ - function contact_url( $topic = false, $message = false ) { + function contact_url( $topic = false, $message = false, $summary = false ) { $params = array(); if ( is_string( $topic ) ) { $params['topic'] = $topic; @@ -16029,6 +16535,10 @@ function contact_url( $topic = false, $message = false ) { $params['message'] = $message; } + if ( is_string( $summary ) ) { + $params['summary'] = $summary; + } + if ( $this->is_addon() ) { $params['addon_id'] = $this->get_id(); @@ -16067,7 +16577,7 @@ function get_addons_url() { } /* Logger - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /** * @param string $id * @param bool $prefix_slug @@ -16092,7 +16602,7 @@ function get_options_manager( $id, $load_options = false, $prefix_slug = true ) } /* Security - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ private static function _encrypt( $str ) { if ( is_null( $str ) ) { return null; @@ -16179,64 +16689,6 @@ private static function decrypt_entity( FS_Entity $entity ) { return $clone; } - /** - * Tries to activate account based on POST params. - * - * @author Vova Feldman (@svovaf) - * @since 1.0.2 - * - * @deprecated Not in use, outdated. - */ - function _activate_account() { - if ( $this->is_registered() ) { - // Already activated. - return; - } - - self::_clean_admin_content_section(); - - if ( fs_request_is_action( 'activate' ) && fs_request_is_post() ) { -// check_admin_referer( 'activate_' . $this->_plugin->public_key ); - - // Verify matching plugin details. - if ( $this->_plugin->id != fs_request_get( 'plugin_id' ) || $this->_slug != fs_request_get( 'plugin_slug' ) ) { - return; - } - - $user = new FS_User(); - $user->id = fs_request_get( 'user_id' ); - $user->public_key = fs_request_get( 'user_public_key' ); - $user->secret_key = fs_request_get( 'user_secret_key' ); - $user->email = fs_request_get( 'user_email' ); - $user->first = fs_request_get( 'user_first' ); - $user->last = fs_request_get( 'user_last' ); - $user->is_verified = fs_request_get_bool( 'user_is_verified' ); - - $site = new FS_Site(); - $site->id = fs_request_get( 'install_id' ); - $site->public_key = fs_request_get( 'install_public_key' ); - $site->secret_key = fs_request_get( 'install_secret_key' ); - $site->plan_id = fs_request_get( 'plan_id' ); - - $plans = array(); - $plans_data = json_decode( urldecode( fs_request_get( 'plans' ) ) ); - foreach ( $plans_data as $p ) { - $plan = new FS_Plugin_Plan( $p ); - if ( $site->plan_id == $plan->id ) { - $plan->title = fs_request_get( 'plan_title' ); - $plan->name = fs_request_get( 'plan_name' ); - } - - $plans[] = $plan; - } - - $this->_set_account( $user, $site, $plans ); - - // Reload the page with the keys. - fs_redirect( $this->_get_admin_page_url() ); - } - } - /** * @author Vova Feldman (@svovaf) * @since 1.0.7 @@ -16312,20 +16764,6 @@ private function _load_account() { ) { // Load site. $this->_site = $site; - - // Load plans. - $this->_plans = $plans[ $this->_slug ]; - if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) { - $this->_sync_plans(); - } else { - for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) { - if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) { - $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] ); - } else { - unset( $this->_plans[ $i ] ); - } - } - } } $user = null; @@ -16354,7 +16792,30 @@ private function _load_account() { /** * This is a special fault tolerance mechanism to handle a scenario that the user data is missing. */ - $user = $this->sync_user_by_current_install(); + if ( + ! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) || + time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME ) + ) { + $user = $this->sync_user_by_current_install(); + } else { + return; + } + + if ( is_object( $user ) ) { + $this->_storage->user_was_recovered_from_install = true; + } else { + $this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ? + ( $this->_storage->user_recovery_from_install_attempts + 1 ) : + 1; + + if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) { + $this->delete_current_install( false ); + } else { + $this->_storage->user_recovery_from_install_last_attempt_timestamp = time(); + + return; + } + } } $this->_user = ( $user instanceof FS_User ) ? @@ -16368,6 +16829,23 @@ private function _load_account() { } if ( is_object( $this->_site ) ) { + // Load plans. + $this->_plans = isset( $plans[ $this->_slug ] ) ? + $plans[ $this->_slug ] : + array(); + + if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) { + $this->_sync_plans(); + } else { + for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) { + if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) { + $this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] ); + } else { + unset( $this->_plans[ $i ] ); + } + } + } + $this->_license = $this->_get_license_by_id( $this->_site->license_id ); if ( $this->_site->version != $this->get_plugin_version() ) { @@ -16386,6 +16864,15 @@ private function _load_account() { if ( $this->is_theme() ) { $this->_register_account_hooks(); } + + if ( $this->is_user_in_admin() && $this->is_clone() ) { + if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) { + FS_Clone_Manager::instance()->store_clone_identification_timestamp(); + } + + FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version ); + $this->send_pending_clone_update_once(); + } } /** @@ -16465,19 +16952,131 @@ private function _set_account( FS_User $user, FS_Site $site, $plans = false ) { */ private function get_versions() { $versions = array(); - $versions['platform_version'] = get_bloginfo( 'version' ); - $versions['sdk_version'] = $this->version; - $versions['programming_language_version'] = phpversion(); + $versions['sdk_version'] = $this->version; + + // Collect these diagnostic information only if it's allowed. + if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) { + $versions['platform_version'] = get_bloginfo( 'version' ); + $versions['programming_language_version'] = phpversion(); + } foreach ( $versions as $k => $version ) { - if ( is_string( $versions[ $k ] ) && ! empty( $versions[ $k ] ) ) { - $versions[ $k ] = substr( $versions[ $k ], 0, 16 ); - } + $versions[ $k ] = self::get_api_sanitized_property( $k, $version ); } return $versions; } + /** + * Get sanitized site language. + * + * @param string $language + * @param int $max_len + * + * @since 2.5.1 + * @author Vova Feldman (@svovaf) + * + * @return string + */ + private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) { + if ( empty( $language ) ) { + $language = get_bloginfo( 'language' ); + } + + return substr( $language, 0, $max_len ); + } + + /** + * Get core version stripped from pre-release and build. + * + * @since 2.5.1 + * @author Vova Feldman (@svovaf) + * + * @param string $version + * @param int $parts + * @param int $max_len + * @param bool $include_pre_release + * + * @return string + */ + private static function get_core_version( + $version, + $parts = 3, + $max_len = self::VERSION_MAX_CHARS, + $include_pre_release = false + ) { + if ( empty( $version ) ) { + // Version is empty. + return ''; + } + + if ( is_numeric( $version ) ) { + $is_float_version = is_float( $version ); + + $version = (string) $version; + + /** + * Casting a whole float number to a string cuts the decimal point. This part make sure to add the missing decimal part to the version. + */ + if ( $is_float_version && false === strpos( $version, '.' ) ) { + $version .= '.0'; + } + } + + if ( ! is_string( $version ) ) { + return ''; + } + + if ( $parts < 1 ) { + return ''; + } + + $pre_release_regex = $include_pre_release ? + '(\-(alpha|beta|RC)([0-9]+)?)?' : + ''; + + if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) { + // Version is not starting with a digit. + return ''; + } + + return substr( $matches[1], 0, $max_len ); + } + + /** + * @param string $prop + * @param mixed $val + * + * @return mixed + *@author Vova Feldman (@svovaf) + * + * @since 2.5.1 + */ + private static function get_api_sanitized_property( $prop, $val ) { + if ( ! is_string( $val ) || empty( $val ) ) { + return $val; + } + + switch ( $prop ) { + case 'programming_language_version': + // Get core PHP version, which can have up to 3 parts (ignore pre-releases). + return self::get_core_version( $val ); + case 'platform_version': + // Get the exact WordPress version, which can have up to 3 parts (including pre-releases). + return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true ); + case 'sdk_version': + // Get the exact SDK version, which can have up to 4 parts. + return self::get_core_version( $val, 4 ); + case 'version': + // Get the entire version but just limited in length. + return substr( $val, 0, self::VERSION_MAX_CHARS ); + case 'language': + return self::get_sanitized_language( $val ); + default: + return $val; + } + } + /** * @author Leo Fajardo (@leorw) * @since 2.3.0 @@ -16530,23 +17129,22 @@ function get_opt_in_params( $override_with = array(), $network_level_or_blog_id $versions = $this->get_versions(); $params = array_merge( $versions, array( - 'user_firstname' => $current_user->user_firstname, - 'user_lastname' => $current_user->user_lastname, - 'user_nickname' => $current_user->user_nicename, - 'user_email' => $current_user->user_email, - 'user_ip' => WP_FS__REMOTE_ADDR, - 'plugin_slug' => $this->_slug, - 'plugin_id' => $this->get_id(), - 'plugin_public_key' => $this->get_public_key(), - 'plugin_version' => $this->get_plugin_version(), - 'return_url' => fs_nonce_url( $return_url, $activation_action ), - 'account_url' => fs_nonce_url( $this->_get_admin_page_url( + 'user_firstname' => $current_user->user_firstname, + 'user_lastname' => $current_user->user_lastname, + 'user_email' => $current_user->user_email, + 'plugin_slug' => $this->_slug, + 'plugin_id' => $this->get_id(), + 'plugin_public_key' => $this->get_public_key(), + 'plugin_version' => $this->get_plugin_version(), + 'return_url' => fs_nonce_url( $return_url, $activation_action ), + 'account_url' => fs_nonce_url( $this->_get_admin_page_url( 'account', array( 'fs_action' => 'sync_user' ) ), 'sync_user' ), - 'is_premium' => $this->is_premium(), - 'is_active' => true, - 'is_uninstalled' => false, + 'is_premium' => $this->is_premium(), + 'is_active' => true, + 'is_uninstalled' => false, + 'is_localhost' => WP_FS__IS_LOCALHOST, ) ); if ( $this->is_addon() ) { @@ -16567,12 +17165,17 @@ function get_opt_in_params( $override_with = array(), $network_level_or_blog_id $site = $this->get_site_info( $site ); - $params = array_merge( $params, array( + $diagnostic_info = array(); + if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) { + $diagnostic_info = array( + 'site_name' => $site['title'], + 'language' => self::get_sanitized_language( $site['language'] ), + ); + } + + $params = array_merge( $params, $diagnostic_info, array( 'site_uid' => $site['uid'], 'site_url' => $site['url'], - 'site_name' => $site['title'], - 'language' => $site['language'], - 'charset' => $site['charset'], ) ); } @@ -16597,6 +17200,10 @@ function get_opt_in_params( $override_with = array(), $network_level_or_blog_id ); } + if ( is_multisite() && function_exists( 'get_network' ) ) { + $params['network_uid'] = $this->get_anonymous_network_id(); + } + return array_merge( $params, $override_with ); } @@ -16615,9 +17222,10 @@ function get_opt_in_params( $override_with = array(), $network_level_or_blog_id * In this case, the user and site info will be sent to the server but no * data will be saved to the WP installation's database. * @param number|bool $trial_plan_id - * @param bool $is_disconnected Whether or not to opt in without tracking. + * @param bool $is_disconnected Whether to opt in without tracking. * @param null|bool $is_marketing_allowed * @param array $sites If network-level opt-in, an array of containing details of sites. + * @param bool $redirect * * @return string|object * @use WP_Error @@ -16631,7 +17239,8 @@ function opt_in( $trial_plan_id = false, $is_disconnected = false, $is_marketing_allowed = null, - $sites = array() + $sites = array(), + $redirect = true ) { $this->_logger->entrance(); @@ -16655,7 +17264,7 @@ function opt_in( $fs_user, false, $trial_plan_id, - true, + $redirect, true, $sites ); @@ -16723,13 +17332,15 @@ function opt_in( $params['is_marketing_allowed'] = $is_marketing_allowed; } - $params['is_disconnected'] = $is_disconnected; - $params['format'] = 'json'; + $params['is_disconnected'] = $is_disconnected; + $params['format'] = 'json'; + $params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed(); + $params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed(); $request = array( 'method' => 'POST', 'body' => $params, - 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30, + 'timeout' => 60, ); $url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' ); @@ -16753,9 +17364,25 @@ function opt_in( $this->maybe_modify_api_curl_error_message( $result ); + if ( FS_Api::is_blocked( $result ) ) { + $result->error->message = $this->generate_api_blocked_notice_message_from_result( $result ); + } + + $is_connected = null; + + if ( empty( $license_key ) && $this->is_enable_anonymous() ) { + $this->skip_connection( fs_is_network_admin() ); + + $is_connected = ( ! FS_Api::is_blocked( $result ) ); + } + + $this->update_connectivity_info( $is_connected ); + return $result; } + $this->update_connectivity_info( true ); + // Module is being uninstalled, don't handle the returned data. if ( $is_uninstall ) { return true; @@ -16815,7 +17442,8 @@ function opt_in( true ), false, $filtered_license_key, - ! empty( $params['trial_plan_id'] ) + ! empty( $params['trial_plan_id'] ), + isset( $decoded->is_suspicious_email ) && $decoded->is_suspicious_email ); } else if ( isset( $decoded->install_secret_key ) ) { return $this->install_with_new_user( @@ -16828,6 +17456,9 @@ function opt_in( ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ? $decoded->is_extensions_tracking_allowed : null ), + ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ? + $decoded->is_diagnostic_tracking_allowed : + null ), $decoded->install_id, $decoded->install_public_key, $decoded->install_secret_key, @@ -16844,6 +17475,9 @@ function opt_in( ( isset( $decoded->is_extensions_tracking_allowed ) && ! is_null( $decoded->is_extensions_tracking_allowed ) ? $decoded->is_extensions_tracking_allowed : null ), + ( isset( $decoded->is_diagnostic_tracking_allowed ) && ! is_null( $decoded->is_diagnostic_tracking_allowed ) ? + $decoded->is_diagnostic_tracking_allowed : + null ), $decoded->installs, false ); @@ -16925,6 +17559,9 @@ function setup_network_account( // Only network level opt-in can have more than one install. $is_network_level_opt_in = true; } + + $this->update_connectivity_info( true ); + // $is_network_level_opt_in = self::is_ajax_action_static( 'network_activate', $this->_module_id ); // If Freemius was OFF before, turn it on. $this->turn_on(); @@ -16941,15 +17578,11 @@ function setup_network_account( $this->_admin_notices->remove_sticky( 'connect_account' ); if ( $this->is_pending_activation() || ! $this->has_settings_menu() ) { - // Remove pending activation sticky notice (if still exist). - $this->_admin_notices->remove_sticky( 'activation_pending' ); - - // Remove plugin from pending activation mode. - unset( $this->_storage->is_pending_activation ); + $this->clear_pending_activation_mode(); if ( ! $this->is_paying_or_trial() ) { $this->_admin_notices->add_sticky( - sprintf( $this->get_text_inline( '%s activation was successfully completed.', 'plugin-x-activation-message' ), '' . $this->get_plugin_name() . '' ), + sprintf( $this->get_text_inline( '%s opt-in was successfully completed.', 'plugin-x-activation-message' ), '' . $this->get_plugin_name() . '' ), 'activation_complete' ); } @@ -16961,24 +17594,23 @@ function setup_network_account( ! $this->has_settings_menu() ) { if ( $this->is_paying() ) { - $this->_admin_notices->add_sticky( + $this->add_complete_upgrade_instructions_notice( sprintf( $this->get_text_inline( 'Your account was successfully activated with the %s plan.', 'activation-with-plan-x-message' ), $this->get_plan_title() - ) . $this->get_complete_upgrade_instructions(), - 'plan_upgraded', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + ), + 'plan_upgraded' ); } else { $trial_plan = $this->get_trial_plan(); - $this->_admin_notices->add_sticky( + $this->add_complete_upgrade_instructions_notice( sprintf( $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ), '' . $this->get_plugin_name() . '' - ) . $this->get_complete_upgrade_instructions( $trial_plan->title ), + ), 'trial_started', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + $trial_plan->title ); } } @@ -17055,6 +17687,10 @@ function _install_with_new_user() { return; } + $has_pending_activation_confirmation_param = fs_request_has( 'pending_activation' ); + + $this->update_license_required_permissions_if_anonymous(); + if ( ( $this->is_plugin() && fs_request_is_action( $this->get_unique_affix() . '_activate_new' ) ) || // @todo This logic should be improved because it's executed on every load of a theme. $this->is_theme() @@ -17067,10 +17703,11 @@ function _install_with_new_user() { $this->install_many_pending_with_user( fs_request_get( 'user_id' ), - fs_request_get( 'user_public_key' ), - fs_request_get( 'user_secret_key' ), + fs_request_get_raw( 'user_public_key' ), + fs_request_get_raw( 'user_secret_key' ), fs_request_get_bool( 'is_marketing_allowed', null ), fs_request_get_bool( 'is_extensions_tracking_allowed', null ), + fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ), $pending_sites_info['blog_ids'], $pending_sites_info['license_key'], $pending_sites_info['trial_plan_id'] @@ -17078,19 +17715,28 @@ function _install_with_new_user() { } else { $this->install_with_new_user( fs_request_get( 'user_id' ), - fs_request_get( 'user_public_key' ), - fs_request_get( 'user_secret_key' ), + fs_request_get_raw( 'user_public_key' ), + fs_request_get_raw( 'user_secret_key' ), fs_request_get_bool( 'is_marketing_allowed', null ), fs_request_get_bool( 'is_extensions_tracking_allowed', null ), + fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ), fs_request_get( 'install_id' ), - fs_request_get( 'install_public_key' ), - fs_request_get( 'install_secret_key' ), + fs_request_get_raw( 'install_public_key' ), + fs_request_get_raw( 'install_secret_key' ), true, fs_request_get_bool( 'auto_install' ) ); } - } else if ( fs_request_has( 'pending_activation' ) ) { - $this->set_pending_confirmation( fs_request_get( 'user_email' ), true ); + } else if ( $has_pending_activation_confirmation_param ) { + $this->set_pending_confirmation( + fs_request_get( 'user_email' ), + true, + false, + false, + fs_request_get_bool( 'is_suspicious_email' ), + fs_request_get_bool( 'has_upgrade_context' ), + fs_request_get( 'support_email_address' ) + ); } } } @@ -17138,6 +17784,7 @@ private function setup_user( $id, $public_key, $secret_key ) { * @param string $user_secret_key * @param bool|null $is_marketing_allowed * @param bool|null $is_extensions_tracking_allowed Since 2.3.2 + * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 * @param number $install_id * @param string $install_public_key * @param string $install_secret_key @@ -17152,6 +17799,7 @@ private function install_with_new_user( $user_secret_key, $is_marketing_allowed, $is_extensions_tracking_allowed, + $is_diagnostic_tracking_allowed, $install_id, $install_public_key, $install_secret_key, @@ -17185,7 +17833,7 @@ private function install_with_new_user( $site->secret_key = $install_secret_key; $this->_site = $site; - $site_result = $this->get_api_site_scope()->get(); + $site_result = $this->get_api_site_scope( true )->get(); $site = new FS_Site( $site_result ); $this->_site = $site; @@ -17193,7 +17841,10 @@ private function install_with_new_user( $this->disable_opt_in_notice_and_lock_user(); } - $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed ); + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed, + FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed, + ) ); return $this->setup_account( $this->_user, @@ -17214,6 +17865,7 @@ private function install_with_new_user( * @param string $user_secret_key * @param bool|null $is_marketing_allowed * @param bool|null $is_extensions_tracking_allowed Since 2.3.2 + * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 * @param array $site_ids * @param bool $license_key * @param bool $trial_plan_id @@ -17227,6 +17879,7 @@ private function install_many_pending_with_user( $user_secret_key, $is_marketing_allowed, $is_extensions_tracking_allowed, + $is_diagnostic_tracking_allowed, $site_ids, $license_key = false, $trial_plan_id = false, @@ -17238,7 +17891,10 @@ private function install_many_pending_with_user( $this->disable_opt_in_notice_and_lock_user(); } - $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed ); + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed, + FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed, + ) ); $sites = array(); foreach ( $site_ids as $site_id ) { @@ -17259,6 +17915,7 @@ private function install_many_pending_with_user( * @param string $user_secret_key * @param bool|null $is_marketing_allowed * @param bool|null $is_extensions_tracking_allowed Since 2.3.2 + * @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 * @param object[] $installs * @param bool $redirect * @param bool $auto_install Since 1.2.1.7 If `true` and setting up an account with a valid license, will redirect (or return a URL) to the account page with a special parameter to trigger the auto installation processes. @@ -17271,6 +17928,7 @@ private function install_many_with_new_user( $user_secret_key, $is_marketing_allowed, $is_extensions_tracking_allowed, + $is_diagnostic_tracking_allowed, array $installs, $redirect = true, $auto_install = false @@ -17281,7 +17939,10 @@ private function install_many_with_new_user( $this->disable_opt_in_notice_and_lock_user(); } - $this->update_extensions_tracking_flag( $is_extensions_tracking_allowed ); + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + FS_Permission_Manager::PERMISSION_DIAGNOSTIC => $is_diagnostic_tracking_allowed, + FS_Permission_Manager::PERMISSION_EXTENSIONS => $is_extensions_tracking_allowed, + ) ); $install_ids = array(); @@ -17289,12 +17950,13 @@ private function install_many_with_new_user( $install_ids[] = $install->id; } - $left = count( $install_ids ); - $offset = 0; + $items_per_request = 25; + $left = count( $install_ids ); + $offset = 0; $installs = array(); while ( $left > 0 ) { - $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, 25 ) ) ); + $result = $this->get_api_user_scope()->get( "/plugins/{$this->_module_id}/installs.json?ids=" . implode( ',', array_slice( $install_ids, $offset, $items_per_request ) ) ); if ( ! $this->is_api_result_object( $result, 'installs' ) ) { // @todo Handle API error. @@ -17302,7 +17964,8 @@ private function install_many_with_new_user( $installs = array_merge( $installs, $result->installs ); - $left -= 25; + $left -= $items_per_request; + $offset += $items_per_request; } foreach ( $installs as &$install ) { @@ -17325,6 +17988,9 @@ private function install_many_with_new_user( * @param bool $redirect * @param string|bool $license_key Since 1.2.1.5 * @param bool $is_pending_trial Since 1.2.1.5 + * @param bool $is_suspicious_email Since 2.5.0 + * @param bool $has_upgrade_context Since 2.5.3 + * @param bool|string $support_email_address Since 2.5.3 * * @return string Since 1.2.1.5 if $redirect is `false`, return the pending activation page. */ @@ -17332,22 +17998,33 @@ private function set_pending_confirmation( $email = false, $redirect = true, $license_key = false, - $is_pending_trial = false + $is_pending_trial = false, + $is_suspicious_email = false, + $has_upgrade_context = false, + $support_email_address = false ) { - if ( $this->_ignore_pending_mode ) { + $is_network_admin = fs_is_network_admin(); + + if ( $this->_ignore_pending_mode && ! $has_upgrade_context ) { /** * If explicitly asked to ignore pending mode, set to anonymous mode - * if require confirmation before finalizing the opt-in. + * if require confirmation before finalizing the opt-in except after completing a purchase (otherwise, in this case, they wouldn't see any notice telling them that they should receive their license key via email). * * @author Vova Feldman * @since 1.2.1.6 */ - $this->skip_connection( null, fs_is_network_admin() ); + $this->skip_connection( $is_network_admin ); } else { // Install must be activated via email since // user with the same email already exist. $this->_storage->is_pending_activation = true; - $this->_add_pending_activation_notice( $email, $is_pending_trial ); + $this->_add_pending_activation_notice( + $email, + $is_pending_trial, + $is_suspicious_email, + $has_upgrade_context, + $support_email_address + ); } if ( ! empty( $license_key ) ) { @@ -17362,8 +18039,8 @@ private function set_pending_confirmation( $next_page = $this->get_after_activation_url( 'after_pending_connect_url' ); - // Reload the page with with pending activation message. if ( $redirect ) { + // Reload the page with a pending activation message. fs_redirect( $next_page ); } @@ -17384,15 +18061,18 @@ function _install_with_current_user() { } if ( fs_request_is_action( $this->get_unique_affix() . '_activate_existing' ) && fs_request_is_post() ) { -// check_admin_referer( 'activate_existing_' . $this->_plugin->public_key ); + check_admin_referer( $this->get_unique_affix() . '_activate_existing' ); /** * @author Vova Feldman (@svovaf) * @since 1.1.9 Add license key if given. */ - $license_key = fs_request_get( 'license_secret_key' ); + $license_key = fs_request_get_raw( 'license_secret_key' ); - $this->update_extensions_tracking_flag( fs_request_get_bool( 'is_extensions_tracking_allowed', null ) ); + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array( + FS_Permission_Manager::PERMISSION_DIAGNOSTIC => fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ), + FS_Permission_Manager::PERMISSION_EXTENSIONS => fs_request_get_bool( 'is_extensions_tracking_allowed', null ), + ) ); $this->install_with_current_user( $license_key ); } @@ -17410,7 +18090,7 @@ function _install_with_current_user() { * * @return object|string If redirect is `false`, returns the next page the user should be redirected to, or the API error object if failed to install. */ - private function install_with_current_user( + function install_with_current_user( $license_key = false, $trial_plan_id = false, $sites = array(), @@ -17607,6 +18287,14 @@ private function _activate_addon_account( return; } + $permission_ids = FS_Permission_Manager::get_all_permission_ids(); + $permissions = array(); + foreach ( $permission_ids as $permission_id ) { + $permissions[ $permission_id ] = FS_Permission_Manager::instance( $parent_fs )->is_permission( $permission_id, true ); + } + + FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( $permissions ); + /** * Do not override the `uid` if network-level opt-in since the call to `get_sites_for_network_level_optin()` * already returns the data for the current blog. @@ -17793,9 +18481,6 @@ private function handle_account_connection( $installs, $is_site_level ) { $this->send_installs_update(); } - // Switch install context back to the first install. - $this->_site = $first_install; - $current_blog = get_current_blog_id(); foreach ( $blog_2_install_map as $blog_id => $install ) { @@ -17804,7 +18489,12 @@ private function handle_account_connection( $installs, $is_site_level ) { $this->do_action( 'after_account_connection', $this->_user, $install ); } - $this->switch_to_blog( $current_blog ); + // Switch install context back to the first install. + $this->switch_to_blog( + $current_blog, + $first_install, + ( $this->_site->id != $first_install->id ) + ); $this->do_action( 'after_network_account_connection', $this->_user, $blog_2_install_map ); } @@ -17852,9 +18542,7 @@ private function activate_parent_account( Freemius $parent_fs ) { $parent_fs->_admin_notices->remove_sticky( 'connect_account' ); if ( $parent_fs->is_pending_activation() ) { - $parent_fs->_admin_notices->remove_sticky( 'activation_pending' ); - - unset( $parent_fs->_storage->is_pending_activation ); + $parent_fs->clear_pending_activation_mode(); } // Get user information based on parent's plugin. @@ -17868,6 +18556,8 @@ private function activate_parent_account( Freemius $parent_fs ) { // Sync add-on plans. $parent_fs->_sync_plans(); + $parent_fs->update_license_required_permissions_if_anonymous(); + $parent_fs->_set_account( $user, $parent_fs->_site ); } @@ -17908,6 +18598,10 @@ function _prepare_admin_menu() { // return; // } + if ( is_object( $this->_site ) && ! $this->is_registered() ) { + return; + } + /** * When running from a site admin with a network activated module and the connection * was NOT delegated and the user still haven't skipped or opted-in, then hide the @@ -17926,7 +18620,7 @@ function _prepare_admin_menu() { $should_hide_site_admin_settings = $this->apply_filters( 'should_hide_site_admin_settings_on_network_activation_mode', $should_hide_site_admin_settings ); - if ( ( ! $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) || + if ( ( false === $this->has_api_connectivity() && ! $this->is_enable_anonymous() ) || $should_hide_site_admin_settings ) { $this->_menu->remove_menu_item( $should_hide_site_admin_settings ); @@ -18023,7 +18717,7 @@ private function override_plugin_menu_with_activation() { if ( ! $this->has_settings_menu() ) { // Add the opt-in page without a menu item. $hook = FS_Admin_Menu_Manager::add_subpage( - null, + '', $this->get_plugin_name(), $this->get_plugin_name(), 'manage_options', @@ -18455,7 +19149,7 @@ private function embed_submenu_items() { $hook = FS_Admin_Menu_Manager::add_subpage( $item['show_submenu'] ? $top_level_menu_slug : - null, + '', $item['page_title'], $menu_item, $capability, @@ -18470,7 +19164,7 @@ private function embed_submenu_items() { FS_Admin_Menu_Manager::add_subpage( $item['show_submenu'] ? $top_level_menu_slug : - null, + '', $item['page_title'], $menu_item, $capability, @@ -18862,7 +19556,7 @@ function check_ajax_referer( $tag ) { * * @return string */ - private static function get_ajax_action_static( $tag, $module_id = null ) { + static function get_ajax_action_static( $tag, $module_id = null ) { $action = "fs_{$tag}"; if ( ! empty( $module_id ) ) { @@ -18885,10 +19579,10 @@ private static function get_ajax_action_static( $tag, $module_id = null ) { * @uses do_action() */ function do_action( $tag, $arg = '' ) { - $this->_logger->entrance( $tag ); - $args = func_get_args(); + $this->_logger->entrance( $tag ); + call_user_func_array( 'do_action', array_merge( array( $this->get_action_tag( $tag ) ), array_slice( $args, 1 ) ) @@ -19028,6 +19722,30 @@ static function shoot_ajax_failure( $error = '' ) { wp_send_json( $result ); } + /** + * Returns an AJAX URL with a special extra param to indicate whether the request was triggered from the network admin or blog admin. + * + * @author Vova Feldman (@svovaf) + * @since 2.5.1 + * + * @param string $wrap_with By default, returns the AJAX URL wrapped with single quotes. + * + * @return string + */ + static function ajax_url( $wrap_with = "'") { + if ( fs_is_network_admin() ) { + $param_name = '_fs_network_admin'; + } else { + $param_name = '_fs_blog_admin'; + } + + $url = admin_url( 'admin-ajax.php', 'relative' ); + $url .= ( false === strpos( $url, '?' ) ) ? '?' : '&'; + $url .= "{$param_name}=true"; + + return "{$wrap_with}{$url}{$wrap_with}"; + } + /** * Apply filter, specific for the current context plugin. * @@ -19042,9 +19760,10 @@ static function shoot_ajax_failure( $error = '' ) { * @uses apply_filters() */ function apply_filters( $tag, $value ) { + $args = func_get_args(); + $this->_logger->entrance( $tag ); - $args = func_get_args(); array_unshift( $args, $this->get_unique_affix() ); return call_user_func_array( 'fs_apply_filter', $args ); @@ -19105,7 +19824,7 @@ function override_i18n( $key_value ) { } /* Account Page - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /** * Update site information. * @@ -19116,7 +19835,7 @@ function override_i18n( $key_value ) { * @param null|int $network_level_or_blog_id Since 2.0.0 * @param \FS_Site $site Since 2.0.0 */ - private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null ) { + private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) { $this->_logger->entrance(); if ( is_null( $site ) ) { @@ -19131,9 +19850,12 @@ private function _store_site( $store = true, $network_level_or_blog_id = null, F $site_clone = clone $site; - $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id ); + $sites = self::get_all_sites( $this->_module_type, $network_level_or_blog_id, $is_backup ); - if ( is_object( $this->_user ) && $this->_user->id != $site->user_id ) { + if ( + ! $is_backup && + is_object( $this->_user ) && $this->_user->id != $site->user_id + ) { $this->sync_user_by_current_install( $site->user_id ); $prev_stored_user_id = $this->_storage->get( 'prev_user_id', false, $network_level_or_blog_id ); @@ -19156,9 +19878,28 @@ private function _store_site( $store = true, $network_level_or_blog_id = null, F } } - $sites[ $this->_slug ] = $site_clone; + $sites[ $this->_slug ] = $site_clone; + + $this->set_account_option( + ( $is_backup ? 'prev_' : '' ) . 'sites', + $sites, + $store, + $network_level_or_blog_id + ); + } + + /** + * Stores the context site in the sites backup storage. This logic is used before deleting the site info so that it can be restored later on if necessary (e.g., if the automatic clone resolution attempt fails). + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + private function back_up_site() { + $this->_logger->entrance(); - $this->set_account_option( 'sites', $sites, $store, $network_level_or_blog_id ); + $site_clone = clone $this->_site; + + $this->_store_site( true, null, $site_clone, true ); } /** @@ -20061,7 +20802,7 @@ function _set_data_debug_mode() { return; } - $license_or_user_key = fs_request_get( 'license_or_user_key' ); + $license_or_user_key = fs_request_get_raw( 'license_or_user_key' ); $transient_value = ( ! empty( $license_or_user_key ) ) ? 'true' : @@ -20399,10 +21140,10 @@ private function _sync_plugin_license( $this->switch_to_blog( $current_blog_id ); } - $result = $this->send_install_update( array(), true ); + $result = $this->send_install_update( array(), true, true ); $is_valid = $this->is_api_result_entity( $result ); } else { - $result = $this->send_installs_update( array(), true ); + $result = $this->send_installs_update( array(), true, true ); $is_valid = $this->is_api_result_object( $result, 'installs' ); } @@ -20412,39 +21153,55 @@ private function _sync_plugin_license( $this->switch_to_blog( $this->_storage->network_install_blog_id ); } - // Show API messages only if not background sync or if paying customer. + // Show API message only if not background sync or if paying customer. if ( ! $background || $this->is_paying() ) { // Try to ping API to see if not blocked. - if ( ! FS_Api::test() ) { + if ( FS_Api::is_blocked( $result ) ) { /** - * Failed to ping API - blocked! - * * @author Vova Feldman (@svovaf) * @since 1.1.6 Only show message related to one of the Freemius powered plugins. Once it will be resolved it will fix the issue for all plugins anyways. There's no point to scare users with multiple error messages. */ - $api = $this->get_api_site_scope(); - if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) { - self::$_global_admin_notices->add( - sprintf( - $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist %2$s', 'server-blocking-access' ), - $this->get_plugin_name(), - '' . implode( ', ', $this->apply_filters( 'api_domains', array( - 'api.freemius.com', - 'wp.freemius.com' - ) ) ) . '' - ) . '
' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ), - $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...', - 'error', - $background, - 'api_blocked' - ); + // Add notice immediately if not a background sync. + $add_notice = ( ! $background ); + + if ( ! $add_notice ) { + $counter = (int) get_transient( '_fs_api_connection_retry_counter' ); + + // We only want to add the notice after 3 consecutive failures. + $add_notice = ( 3 <= $counter ); + + if ( ! $add_notice ) { + /** + * Update counter transient only if notice shouldn't be added. If it is added the transient will be reset anyway, because the retries mechanism should only start counting if the admin isn't aware of the connectivity issue. + * + * Also, since the background sync happens once a day, setting the transient expiration for a week should be enough to count 3 failures, if there's an actual connectivity issue. + */ + set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC ); + } + } + + // Add notice instantly for not-background sync and only after 3 failed attempts for background sync. + if ( $add_notice ) { + self::$_global_admin_notices->add( + $this->generate_api_blocked_notice_message_from_result( $result ), + '', + 'error', + $background, + 'api_blocked' + ); + + add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) ); + + // Notice was just shown, reset connectivity counter. + delete_transient( '_fs_api_connection_retry_counter' ); + } } - } else { + } else if ( is_object( $result ) ) { // Authentication params are broken. $this->_admin_notices->add( $this->get_text_inline( 'It seems like one of the authentication parameters is wrong. Update your Public Key, Secret Key & User ID, and try again.', 'wrong-authentication-param-message' ) . '
' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ), - $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...', + '', 'error' ); } @@ -20454,6 +21211,9 @@ private function _sync_plugin_license( return; } + // API is working now. Delete the transient and start afresh. + delete_transient('_fs_api_connection_retry_counter'); + if ( $is_site_level_sync ) { $site = new FS_Site( $result ); } else { @@ -20645,7 +21405,7 @@ private function _sync_plugin_license( } if ( ! $this->is_addon() && - $this->_site->is_beta() !== $site->is_beta + $this->_site->is_beta() !== $site->is_beta() ) { // Beta flag updated. $this->_site = $site; @@ -20703,14 +21463,7 @@ private function _sync_plugin_license( break; case 'upgraded': case 'activated': - $this->_admin_notices->add_sticky( - ( 'activated' === $plan_change ) ? - $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ) : - $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) . - $this->get_complete_upgrade_instructions(), - 'plan_upgraded', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' - ); + $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change ); $this->_admin_notices->remove_sticky( array( 'trial_started', @@ -20772,13 +21525,13 @@ private function _sync_plugin_license( $this->_admin_notices->remove_sticky( 'plan_upgraded' ); break; case 'trial_started': - $this->_admin_notices->add_sticky( + $this->add_complete_upgrade_instructions_notice( sprintf( $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ), '' . $this->get_plugin_name() . '' - ) . $this->get_complete_upgrade_instructions( $this->get_trial_plan()->title ), + ), 'trial_started', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + $this->get_trial_plan()->title ); $this->_admin_notices->remove_sticky( array( @@ -20815,6 +21568,42 @@ private function _sync_plugin_license( } } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param mixed $result + * + * @return string + */ + private function generate_api_blocked_notice_message_from_result( $result ) { + $api_domains = $this->apply_filters( 'api_domains', array( + 'api.freemius.com', + 'wp.freemius.com', + ) ); + + $api_domains_list_items = ''; + + foreach( $api_domains as $api_domain ) { + $api_domains_list_items .= "
  • {$api_domain}
  • "; + } + + $error_message = sprintf( + $this->get_text_inline( 'Your server is blocking the access to Freemius\' API, which is crucial for %1$s synchronization. Please contact your host to whitelist the following domains:%2$s', 'server-blocking-access' ), + $this->get_plugin_name(), + "
      {$api_domains_list_items}
    " . $this->get_text_inline( 'Show error details', 'show-error-details' ) . " " + ); + + $error_message = + "
    {$error_message}
    " . + ''; + + return $error_message; + } + /** * Include the required JS at the footer of the admin to trigger the license activation dialog box. * @@ -20946,11 +21735,9 @@ protected function _activate_license( $background = false, $premium_license = nu } if ( ! $background ) { - $this->_admin_notices->add_sticky( - $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ) . - $this->get_complete_upgrade_instructions(), - 'license_activated', - $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + $this->add_complete_upgrade_instructions_notice( + $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ), + 'license_activated' ); } @@ -21200,8 +21987,7 @@ function start_trial( $plan_name = false ) { if ( ! $this->is_api_result_entity( $plan ) ) { // Some API error while trying to start the trial. $this->_admin_notices->add( - sprintf( $this->get_text_inline( 'Unexpected API error. Please contact the %s\'s author with the following error.', 'unexpected-api-error' ), $this->_module_type ) - . ' ' . var_export( $plan, true ), + $this->get_api_error_message( $plan ), $oops_text, 'error' ); @@ -21379,13 +22165,18 @@ function _fetch_latest_version( ) { $this->_logger->entrance(); + if ( $this->is_unresolved_clone( true ) ) { + return false; + } + $switch_to_blog_id = null; /** * @since 1.1.7.3 Check for plugin updates from Freemius only if opted-in. * @since 1.1.7.4 Also check updates for add-ons. */ - if ( ! $this->is_registered() && + if ( + ( ! $this->is_registered() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) && ! $this->_is_addon_id( $addon_id ) ) { if ( ! is_multisite() ) { @@ -21395,6 +22186,10 @@ function _fetch_latest_version( $installs_map = $this->get_blog_install_map(); foreach ( $installs_map as $blog_id => $install ) { + if ( ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed( $blog_id ) ) { + continue; + } + /** * @var FS_Site $install */ @@ -21491,9 +22286,11 @@ private function download_latest_directly( $plugin_id = false ) { private function get_latest_download_api_url( $plugin_id = false ) { $this->_logger->entrance(); - return $this->get_api_site_scope()->get_signed_url( + $download_api_url = $this->get_api_site_scope()->get_signed_url( $this->_get_latest_version_endpoint( $plugin_id, 'zip' ) ); + + return str_replace( 'http:', 'https:', $download_api_url ); } /** @@ -21688,7 +22485,6 @@ private function sync_addons( $flush = false ) { private function update_email( $new_email ) { $this->_logger->entrance(); - $api = $this->get_api_user_scope(); $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,email,is_verified", 'put', array( 'email' => $new_email, @@ -21704,7 +22500,6 @@ private function update_email( $new_email ) { $this->_store_user(); } else { // handle different error cases. - } return $user; @@ -21780,15 +22575,32 @@ private function is_array_instanceof( $array, $class ) { * @uses FS_Api * * @param string $candidate_email + * @param string $transfer_type * * @return bool Is ownership change successfully initiated. */ - private function init_change_owner( $candidate_email ) { + private function init_change_owner( $candidate_email, $transfer_type ) { $this->_logger->entrance(); + $installs_info_by_slug_map = $this->get_parent_and_addons_installs_info(); + $install_ids = array(); + + foreach ( $installs_info_by_slug_map as $slug => $install_info ) { + $install = $install_info['install']; + + if ( $this->_user->id != $install->user_id ) { + // Skip add-on installs that are not owned by the parent product's install's owner. + continue; + } + + $install_ids[ $slug ] = $install->id; + } + $api = $this->get_api_site_scope(); $result = $api->call( "/users/{$this->_user->id}.json", 'put', array( 'email' => $candidate_email, + 'transfer_type' => $transfer_type, + 'install_ids' => implode( ',', array_values( $install_ids ) ), 'after_confirm_url' => $this->_get_admin_page_url( 'account', array( 'fs_action' => 'change_owner' ) @@ -21810,28 +22622,113 @@ private function init_change_owner( $candidate_email ) { private function complete_change_owner() { $this->_logger->entrance(); - $site_result = $this->get_api_site_scope( true )->get(); - $site = new FS_Site( $site_result ); - $this->_site = $site; + $install_ids = fs_request_get( 'install_ids' ); + + if ( ! empty( $install_ids ) ) { + $install_ids = explode( ',', $install_ids ); + + foreach ( $install_ids as $key => $install_id ) { + if ( ! FS_Site::is_valid_id( $install_id ) ) { + unset( $install_ids[ $key ] ); + } + } + } + + if ( ! is_array( $install_ids ) ) { + $install_ids = array(); + } + + $user = new FS_User(); + $user->id = fs_request_get( 'user_id' ); + $user->public_key = fs_request_get_raw( 'user_public_key' ); + $user->secret_key = fs_request_get_raw( 'user_secret_key' ); + + $prev_user = $this->_user; + $this->_user = $user; + + $result = $this->get_api_user_scope( true )->get( + "/installs.json?install_ids=" . implode( ',', $install_ids ) + ); + + $current_blog_sites = self::get_all_sites( $this->get_module_type() ); + + if ( $this->is_api_result_object( $result, 'installs' ) ) { + $site_id_slug_map = array(); + + foreach ( $current_blog_sites as $slug => $site ) { + $site_id_slug_map[ $site->id ] = $slug; + } + + foreach ( $result->installs as $install ) { + $site = new FS_Site( $install ); + + if ( ! isset( $site_id_slug_map[ $install->id ] ) ) { + continue; + } - $user = new FS_User(); - $user->id = fs_request_get( 'user_id' ); + $current_blog_sites[ $site_id_slug_map[ $install->id ] ] = clone $site; + + if ( $this->_site->id == $site->id ) { + $this->_site = $site; + } + } + } // Validate install's user and given user. if ( $user->id != $this->_site->user_id ) { + $this->_user = $prev_user; + return false; } - $user->public_key = fs_request_get( 'user_public_key' ); - $user->secret_key = fs_request_get( 'user_secret_key' ); + $this->set_account_option( 'sites', $current_blog_sites, true ); // Fetch new user information. - $this->_user = $user; $user_result = $this->get_api_user_scope( true )->get(); $user = new FS_User( $user_result ); $this->_user = $user; - $this->_set_account( $user, $site ); + $this->_set_account( $user, $this->_site ); + + $remove_user = true; + $all_modules_sites = self::get_all_modules_sites(); + + foreach ( $all_modules_sites as $sites_by_module_type ) { + foreach ( $sites_by_module_type as $sites_by_slug ) { + foreach ( $sites_by_slug as $site ) { + if ( $prev_user->id == $site->user_id ) { + $remove_user = false; + break; + } + } + + if ( ! $remove_user ) { + break; + } + } + + if ( ! $remove_user ) { + break; + } + } + + if ( $remove_user ) { + $users = self::get_all_users(); + + if ( isset( $users[ $prev_user->id ] ) ) { + unset( $users[ $prev_user->id ] ); + } else { + // If the prev user wasn't found by the key, iterate over the users collection. + foreach ( $users as $key => $user ) { + if ( $user->id == $prev_user->id ) { + unset( $users[ $key ] ); + break; + } + } + } + + $this->set_account_option( 'users', $users, true ); + } return true; } @@ -22076,26 +22973,23 @@ private function _handle_account_edits() { if ( $is_parent_plugin_action ) { if ( $is_network_action && ! empty( $blog_id ) ) { - if ( $this->is_registered() ) { - if ( $this->is_tracking_prohibited() ) { - if ( $this->allow_site_tracking() ) { + if ( $this->is_registered( true ) ) { + if ( $this->is_tracking_prohibited( $blog_id ) ) { + if ( $this->toggle_site_tracking( true, $blog_id ) ) { $this->_admin_notices->add( - sprintf( $this->get_text_inline( 'We appreciate your help in making the %s better by letting us track some usage data.', 'opt-out-message-appreciation' ), $this->_module_type ), + sprintf( $this->get_text_inline( 'Sharing diagnostic data with %s helps to provide functionality that\'s more relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the plugin should be translated and tailored to.', 'opt-out-message-appreciation' ), "{$this->get_plugin_title()}" ), $this->get_text_inline( 'Thank you!', 'thank-you' ) ); } } else { - if ( $this->stop_site_tracking() ) { + if ( $this->toggle_site_tracking( false, $blog_id ) ) { + $install = $this->get_install_by_blog_id( $blog_id ); + $this->_admin_notices->add( sprintf( - $this->get_text_inline( 'We will no longer be sending any usage data of %s on %s to %s.', 'opted-out-successfully' ), - $this->get_plugin_title(), - fs_strip_url_protocol( get_site_url( $blog_id ) ), - sprintf( - '%s', - 'https://freemius.com', - 'freemius.com' - ) + $this->get_text_inline( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ), + self::get_unfiltered_site_url( $blog_id, true ), + "{$this->get_plugin_title()}" ) ); } @@ -22243,18 +23137,32 @@ private function _handle_account_edits() { $state = fs_request_get( 'state', 'init' ); switch ( $state ) { case 'init': - $candidate_email = fs_request_get( 'candidate_email', '' ); + // The nonce is injected by the error handler in `_email_address_update_ajax_handler` function. + check_admin_referer( 'change_owner' ); + + $candidate_email = fs_request_get( 'candidate_email' ); + $transfer_type = fs_request_get( 'transfer_type' ); - if ( $this->init_change_owner( $candidate_email ) ) { - $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Please check your mailbox, you should receive an email via %s to confirm the ownership change. From security reasons, you must confirm the change within the next 15 min. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '' . $this->_user->email . '' ) ); + if ( $this->init_change_owner( $candidate_email, $transfer_type ) ) { + if ( 'transfer' === $transfer_type ) { + $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours.', 'change-owner-request-sent-x-transfer' ), '' . $this->_user->email . '' ) ); + } else { + $this->_admin_notices->add( sprintf( $this->get_text_inline( 'A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder.', 'change-owner-request-sent-x' ), '' . $this->_user->email . '' ) ); + } } break; case 'owner_confirmed': + // We cannot (or need not to) check the nonce and referer here, because the link comes from the email sent by our API. $candidate_email = fs_request_get( 'candidate_email', '' ); + if ( ! is_email($candidate_email ) ) { + return; + } + $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Thanks for confirming the ownership change. An email was just sent to %s for final approval.', 'change-owner-request_owner-confirmed' ), '' . $candidate_email . '' ) ); break; case 'candidate_confirmed': + // We do not need to validate the authenticity of this request here, because the `complete_change_owner` does that for us through API calls. if ( $this->complete_change_owner() ) { $this->_admin_notices->add_sticky( sprintf( $this->get_text_inline( '%s is the new owner of the account.', 'change-owner-request_candidate-confirmed' ), '' . $this->_user->email . '' ), @@ -22269,37 +23177,6 @@ private function _handle_account_edits() { return; - case 'update_email': - check_admin_referer( 'update_email' ); - - $new_email = fs_request_get( 'fs_email_' . $this->get_unique_affix(), '' ); - $result = $this->update_email( $new_email ); - - if ( isset( $result->error ) ) { - switch ( $result->error->code ) { - case 'user_exist': - $this->_admin_notices->add( - $this->get_text_inline( 'Sorry, we could not complete the email update. Another user with the same email is already registered.', 'user-exist-message' ) . ' ' . - sprintf( $this->get_text_inline( 'If you would like to give up the ownership of the %s\'s account to %s click the Change Ownership button.', 'user-exist-message_ownership' ), $this->_module_type, '' . $new_email . '' ) . - sprintf( - '', - $this->get_account_url( 'change_owner', array( - 'state' => 'init', - 'candidate_email' => $new_email - ) ), - $this->get_text_inline( 'Change Ownership', 'change-ownership' ) - ), - $oops_text, - 'error' - ); - break; - } - } else { - $this->_admin_notices->add( $this->get_text_inline( 'Your email was successfully updated. You should receive an email with confirmation instructions in few moments.', 'email-updated-message' ) ); - } - - return; - case 'update_user_name': check_admin_referer( 'update_user_name' ); @@ -22319,6 +23196,10 @@ private function _handle_account_edits() { #region Actions that might be called from external links (e.g. email) + /** + * !!IMPORTANT!!: We cannot check for a valid nonce in this region, because the links could be coming from emails. + */ + case 'cancel_trial': $result = $this->cancel_subscription_or_trial( $plugin_id ); if ( $this->is_api_error( $result ) ) { @@ -22461,7 +23342,26 @@ function _affiliation_page_render() { fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' ); - $vars = array( 'id' => $this->_module_id ); + $is_bundle_context = $this->has_bundle_context(); + + $plugin_title = $this->get_plugin_title(); + + if ( $is_bundle_context ) { + $plugin_title = $this->plugin_affiliate_terms->plugin_title; + + // Add the suffix "Bundle" only if the word is not present in the title itself. + if ( false === mb_stripos( $plugin_title, fs_text_inline( 'Bundle', 'bundle' ) ) ) { + $plugin_title = $this->apply_filters( + 'formatted_bundle_title', + $plugin_title . ' ' . fs_text_inline( 'Bundle', 'bundle' ) + ); + } + } + + $vars = array( + 'id' => $this->_module_id, + 'plugin_title' => $plugin_title, + ); echo $this->apply_filters( "/forms/affiliation.php", fs_get_template( '/forms/affiliation.php', $vars ) ); } @@ -22562,7 +23462,7 @@ function _addons_page_render() { } /* Pricing & Upgrade - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /** * Render pricing page. * @@ -22606,11 +23506,11 @@ function _fs_pricing_ajax_action_handler() { $params = array( 'is_enriched' => true, 'trial' => fs_request_get_bool( 'trial' ), - 'sandbox' => fs_request_get( 'sandbox' ), - 's_ctx_type' => fs_request_get( 's_ctx_type' ), - 's_ctx_id' => fs_request_get( 's_ctx_id' ), - 's_ctx_ts' => fs_request_get( 's_ctx_ts' ), - 's_ctx_secure' => fs_request_get( 's_ctx_secure' ), + 'sandbox' => fs_request_get_raw( 'sandbox' ), + 's_ctx_type' => fs_request_get_raw( 's_ctx_type' ), + 's_ctx_id' => fs_request_get_raw( 's_ctx_id' ), + 's_ctx_ts' => fs_request_get_raw( 's_ctx_ts' ), + 's_ctx_secure' => fs_request_get_raw( 's_ctx_secure' ), ); $bundle_id = $this->get_bundle_id(); @@ -22704,7 +23604,17 @@ private static function _hide_admin_notices() { } static function _clean_admin_content_section_hook() { - self::_hide_admin_notices(); + $hide_admin_notices = true; + + if ( fs_request_is_action( 'allow_clone_resolution_notice' ) ) { + check_admin_referer( 'fs_allow_clone_resolution_notice' ); + + $hide_admin_notices = false; + } + + if ( $hide_admin_notices ) { + self::_hide_admin_notices(); + } // Hide footer. echo ''; @@ -22721,17 +23631,17 @@ static function _clean_admin_content_section() { } /* CSS & JavaScript - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ /* function _enqueue_script($handle, $src) { - $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src ); + $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src ); - $this->_logger->entrance( 'script = ' . $url ); + $this->_logger->entrance( 'script = ' . $url ); - wp_enqueue_script( $handle, $url ); - }*/ + wp_enqueue_script( $handle, $url ); + }*/ /* SDK - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ private $_user_api; /** @@ -22743,7 +23653,7 @@ static function _clean_admin_content_section() { * * @return FS_Api */ - private function get_api_user_scope( $flush = false ) { + function get_api_user_scope( $flush = false ) { if ( ! isset( $this->_user_api ) || $flush ) { $this->_user_api = $this->get_api_user_scope_by_user( $this->_user ); } @@ -22822,13 +23732,56 @@ private function get_api_site_scope( $flush = false ) { $this->_site->public_key, ! $this->is_live(), $this->_site->secret_key, - $this->get_sdk_version() + $this->get_sdk_version(), + self::get_unfiltered_site_url() ); } return $this->_site_api; } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param string $path + * @param string $method + * @param array $params + * @param bool $flush_instance + * + * @return array|mixed|string|void + * @throws Freemius_Exception + */ + private function api_site_call( $path, $method = 'GET', $params = array(), $flush_instance = false ) { + $result = $this->get_api_site_scope( $flush_instance )->call( $path, $method, $params ); + + /** + * Checks if the local install's URL is different from the remote install's URL, update the local install if necessary, and then run the clone handler if the install's URL is different from the URL of the site. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + if ( + $this->is_registered() && + FS_Api::is_api_result_entity( $result ) && + isset( $result->url ) + ) { + $stored_local_url = trailingslashit( $this->_site->url ); + $stored_remote_url = trailingslashit( $result->url ); + + if ( $stored_local_url !== $stored_remote_url ) { + $this->_site->url = $result->url; + $this->_store_site(); + } + + if ( fs_strip_url_protocol( $stored_remote_url ) !== self::get_unfiltered_site_url( null, true, true ) ) { + FS_Clone_Manager::instance()->maybe_run_clone_resolution(); + } + } + + return $result; + } + private $_plugin_api; /** @@ -23264,7 +24217,7 @@ function _add_fs_theme_activation_dialog() { } /* Action Links - ------------------------------------------------------------------------------------------------------------------*/ + ------------------------------------------------------------------------------------------------------------------*/ private $_action_links_hooked = false; private $_action_links = array(); @@ -23435,15 +24388,6 @@ function _add_tracking_links() { $this->_logger->entrance(); - /** - * @author Vova Feldman (@svovaf) - * @since 2.3.2 Allow opting out from usage-tracking for paid products too by giving the appropriate warning letting the user know the automatic updates mechanism cannot function without an ongoing connection to the licensing and updates engine. - */ - /*if ( $this->is_premium() ) { - // Don't add opt-in/out for premium code base. - return; - }*/ - if ( $this->is_only_premium() && $this->is_free_plan() ) { // Don't add tracking links for premium-only products that were opted-in by relation (add-on or a parent product) before activating any license. return; @@ -23451,10 +24395,13 @@ function _add_tracking_links() { if ( $this->is_addon() && - ! $this->is_only_premium() && - $this->_parent->is_anonymous() + ! $this->is_only_premium() ) { - return; + $parent = $this->get_parent_instance(); + + if ( is_object( $parent ) && $parent->is_anonymous() ) { + return; + } } if ( fs_is_network_admin() ) { @@ -23503,23 +24450,15 @@ function _add_tracking_links() { } } - if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) { - return; - } - - if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) { - return; - } - - if ( $this->add_ajax_action( 'update_tracking_permission', array( &$this, '_update_tracking_permission_callback' ) ) ) { + if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) { return; } $link_text_id = ''; $url = '#'; - if ( $this->is_registered() ) { - if ( $this->is_tracking_allowed() ) { + if ( $this->is_registered( true ) ) { + if ( $this->is_registered() && $this->is_tracking_allowed() ) { $link_text_id = $this->get_text_inline( 'Opt Out', 'opt-out' ); } else { $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' ); @@ -23716,9 +24655,12 @@ function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' */ private function is_premium_version_installed() { $premium_plugin_basename = $this->premium_plugin_basename(); - $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) ); - return ! empty( $premium_plugin ); + if ( $this->is_theme() ) { + return $this->can_activate_theme( $this->get_premium_slug() ); + } + + return file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $premium_plugin_basename ) ); } /** @@ -23754,7 +24696,9 @@ private function get_complete_upgrade_instructions( $plan_title = '' ) { * @author Leo Fajardo (@leorw) * @since 2.2.1 */ - $premium_plugin_basename = $this->premium_plugin_basename(); + $premium_theme_slug_or_plugin_basename = $this->is_theme() ? + $this->get_premium_slug() : + $this->premium_plugin_basename(); return sprintf( /* translators: %1$s: Product title; %2$s: Plan title */ @@ -23763,7 +24707,9 @@ private function get_complete_upgrade_instructions( $plan_title = '' ) { $plan_title, sprintf( '', - wp_nonce_url( 'plugins.php?action=activate&plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_plugin_basename ), + ( $this->is_theme() ? + wp_nonce_url( 'themes.php?action=activate&stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) : + wp_nonce_url( 'plugins.php?action=activate&plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ), esc_html( sprintf( /* translators: %s: Plan title */ $this->get_text_inline( 'Activate %s features', 'activate-x-features' ), @@ -23788,12 +24734,48 @@ private function get_complete_upgrade_instructions( $plan_title = '' ) { ) ), $deactivation_step, $this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ), - $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/upload-wp-' . $this->_module_type . 's' ), + $this->apply_filters( 'upload_and_install_video_url', '//bit.ly/wp-' . $this->_module_type . '-upload' ), $this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' ) ); } } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.3 + * + * @param string $message_before_the_instructions + * @param string $message_id + * @param string $plan_title + */ + private function add_complete_upgrade_instructions_notice( + $message_before_the_instructions, + $message_id, + $plan_title = '' + ) { + $this->_admin_notices->add_sticky( + $message_before_the_instructions . + $this->get_complete_upgrade_instructions( $plan_title ), + $message_id, + $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' + ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.3 + * + * @param bool $is_upgrade + */ + private function add_after_plan_activation_or_upgrade_instructions_notice( $is_upgrade = true ) { + $this->add_complete_upgrade_instructions_notice( + $is_upgrade ? + $this->get_text_inline( 'Your plan was successfully upgraded.', 'plan-upgraded-message' ) : + $this->get_text_inline( 'Your plan was successfully activated.', 'plan-activated-message' ), + 'plan_upgraded' + ); + } + /** * @author Leo Fajardo (@leorw) * @since 2.1.0 @@ -23847,25 +24829,21 @@ static function safe_remote_post( self::enrich_request_for_debug( $url, $request ); } - $response = wp_remote_post( $url, $request ); + if ( ! isset( $request['method'] ) ) { + $request['method'] = 'POST'; + } - if ( $response instanceof WP_Error ) { - if ( 'https://' === substr( $url, 0, 8 ) && - isset( $response->errors ) && - isset( $response->errors['http_request_failed'] ) - ) { - $http_error = strtolower( $response->errors['http_request_failed'][0] ); + $response = FS_Api::remote_request( $url, $request ); - if ( false !== strpos( $http_error, 'ssl' ) || - false !== strpos( $http_error, 'curl error 35' ) - ) { - // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare). - $url = 'http://' . substr( $url, 8 ); + if ( + 'https://' === substr( $url, 0, 8 ) && + FS_Api::is_ssl_error_response( $response ) + ) { + // Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare). + $url = 'http://' . substr( $url, 8 ); - $request['timeout'] = 15; - $response = wp_remote_post( $url, $request ); - } - } + $request['timeout'] = 15; + $response = FS_Api::remote_request( $url, $request ); } if ( false !== $cache_key ) { @@ -24241,8 +25219,10 @@ function _add_auto_installation_dialog_box() { function _tabs_capture() { $this->_logger->entrance(); - if ( ! $this->is_product_settings_page() || - ! $this->is_matching_url( $this->main_menu_url() ) + if ( + ! $this->is_product_settings_page() || + ! $this->should_page_include_tabs() || + ! $this->is_matching_url( $this->main_menu_url() ) ) { return; } @@ -24296,8 +25276,10 @@ function _store_tabs_ajax_action() { function _store_tabs_styles() { $this->_logger->entrance(); - if ( ! $this->is_product_settings_page() || - ! $this->is_matching_url( $this->main_menu_url() ) + if ( + ! $this->is_product_settings_page() || + ! $this->should_page_include_tabs() || + ! $this->is_matching_url( $this->main_menu_url() ) ) { return; } @@ -24740,24 +25722,6 @@ function fetch_remote_icon_url() { #region GDPR #-------------------------------------------------------------------------------- - /** - * @author Leo Fajardo (@leorw) - * @since 2.1.0 - * - * @return bool - */ - function fetch_and_store_current_user_gdpr_anonymously() { - $pong = $this->ping( null, true ); - - if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) { - return false; - } else { - FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required ); - - return $pong->is_gdpr_required; - } - } - /** * @author Leo Fajardo (@leorw) * @since 2.1.0 @@ -24876,7 +25840,7 @@ private function get_gdpr_admin_notice_string( $user_plugins ) { '%s %s %s', $thank_you, $already_opted_in, - sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '', '' ) . + sprintf( $this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)', 'due-to-gdpr-compliance-requirements' ), '', '' ) . '

    ' . '' . $this->get_text_inline( "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:", 'contact-for-updates' ) . '' . $actions . @@ -25118,6 +26082,14 @@ private function disable_opt_in_notice_and_lock_user() { FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC ); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + */ + static function _add_api_connectivity_notice_handler_js() { + fs_require_once_template( 'api-connectivity-message-js.php' ); + } + /** * @author Leo Fajardo (@leorw) * @since 2.1.0 @@ -25157,7 +26129,7 @@ function _fetch_is_marketing_required_flag_value_ajax_action() { $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' ); - $license_key = fs_request_get( 'license_key' ); + $license_key = fs_request_get_raw( 'license_key' ); if ( empty($license_key) ) { self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) ); @@ -25211,7 +26183,7 @@ private function fetch_installs_licenses_owners_data( $install_ids ) { '/licenses_owners.json?install_ids=' . implode( ',', $install_ids ) ); - $license_owners = null; + $license_owners = array(); if ( $this->is_api_result_object( $response, 'owners' ) ) { $license_owners = $response->owners; diff --git a/freemius/includes/class-fs-admin-notices.php b/freemius/includes/class-fs-admin-notices.php index 01b197a..a639402 100644 --- a/freemius/includes/class-fs-admin-notices.php +++ b/freemius/includes/class-fs-admin-notices.php @@ -125,13 +125,10 @@ function add( $is_sticky = false, $id = '', $store_if_sticky = true, - $network_level_or_blog_id = null + $network_level_or_blog_id = null, + $is_dimissible = null ) { - if ( $this->should_use_network_notices( $id, $network_level_or_blog_id ) ) { - $notices = $this->_network_notices; - } else { - $notices = $this->get_site_notices( $network_level_or_blog_id ); - } + $notices = $this->get_site_or_network_notices( $id, $network_level_or_blog_id ); $notices->add( $message, @@ -139,7 +136,11 @@ function add( $type, $is_sticky, $id, - $store_if_sticky + $store_if_sticky, + null, + null, + false, + $is_dimissible ); } @@ -149,8 +150,9 @@ function add( * * @param string|string[] $ids * @param int|null $network_level_or_blog_id + * @param bool $store */ - function remove_sticky( $ids, $network_level_or_blog_id = null ) { + function remove_sticky( $ids, $network_level_or_blog_id = null, $store = true ) { if ( ! is_array( $ids ) ) { $ids = array( $ids ); } @@ -161,7 +163,7 @@ function remove_sticky( $ids, $network_level_or_blog_id = null ) { $notices = $this->get_site_notices( $network_level_or_blog_id ); } - return $notices->remove_sticky( $ids ); + return $notices->remove_sticky( $ids, $store ); } /** @@ -176,11 +178,7 @@ function remove_sticky( $ids, $network_level_or_blog_id = null ) { * @return bool */ function has_sticky( $id, $network_level_or_blog_id = null ) { - if ( $this->should_use_network_notices( $id, $network_level_or_blog_id ) ) { - $notices = $this->_network_notices; - } else { - $notices = $this->get_site_notices( $network_level_or_blog_id ); - } + $notices = $this->get_site_or_network_notices( $id, $network_level_or_blog_id ); return $notices->has_sticky( $id ); } @@ -200,6 +198,7 @@ function has_sticky( $id, $network_level_or_blog_id = null ) { * @param string|null $plugin_title * @param bool $is_network_and_blog_admins Whether or not the message should be shown both on network and * blog admin pages. + * @param bool $is_dismissible */ function add_sticky( $message, @@ -209,15 +208,30 @@ function add_sticky( $network_level_or_blog_id = null, $wp_user_id = null, $plugin_title = null, - $is_network_and_blog_admins = false + $is_network_and_blog_admins = false, + $is_dismissible = true, + $data = array() ) { - if ( $this->should_use_network_notices( $id, $network_level_or_blog_id ) ) { - $notices = $this->_network_notices; - } else { - $notices = $this->get_site_notices( $network_level_or_blog_id ); - } + $notices = $this->get_site_or_network_notices( $id, $network_level_or_blog_id ); + + $notices->add_sticky( $message, $id, $title, $type, $wp_user_id, $plugin_title, $is_network_and_blog_admins, $is_dismissible, $data ); + } + + /** + * Retrieves the data of a sticky notice. + * + * @author Leo Fajardo (@leorw) + * @since 2.4.3 + * + * @param string $id + * @param int|null $network_level_or_blog_id + * + * @return array|null + */ + function get_sticky( $id, $network_level_or_blog_id ) { + $notices = $this->get_site_or_network_notices( $id, $network_level_or_blog_id ); - $notices->add_sticky( $message, $id, $title, $type, $wp_user_id, $plugin_title, $is_network_and_blog_admins ); + return $notices->get_sticky( $id ); } /** @@ -227,21 +241,22 @@ function add_sticky( * @since 2.0.0 * * @param int|null $network_level_or_blog_id + * @param bool $is_temporary */ - function clear_all_sticky( $network_level_or_blog_id = null ) { + function clear_all_sticky( $network_level_or_blog_id = null, $is_temporary = false ) { if ( ! $this->_is_multisite || false === $network_level_or_blog_id || 0 == $network_level_or_blog_id || is_null( $network_level_or_blog_id ) ) { $notices = $this->get_site_notices( $network_level_or_blog_id ); - $notices->clear_all_sticky(); + $notices->clear_all_sticky( $is_temporary ); } if ( $this->_is_multisite && ( true === $network_level_or_blog_id || is_null( $network_level_or_blog_id ) ) ) { - $this->_network_notices->clear_all_sticky(); + $this->_network_notices->clear_all_sticky( $is_temporary ); } } @@ -317,5 +332,22 @@ private function should_use_network_notices( $id = '', $network_level_or_blog_id return fs_is_network_admin(); } + /** + * Retrieves an instance of FS_Admin_Notice_Manager. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param string $id + * @param int|null $network_level_or_blog_id + * + * @return FS_Admin_Notice_Manager + */ + private function get_site_or_network_notices( $id, $network_level_or_blog_id ) { + return $this->should_use_network_notices( $id, $network_level_or_blog_id ) ? + $this->_network_notices : + $this->get_site_notices( $network_level_or_blog_id ); + } + #endregion } \ No newline at end of file diff --git a/freemius/includes/class-fs-api.php b/freemius/includes/class-fs-api.php index ec24ea2..ef56fad 100644 --- a/freemius/includes/class-fs-api.php +++ b/freemius/includes/class-fs-api.php @@ -64,6 +64,14 @@ class FS_Api { */ private $_sdk_version; + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @var string + */ + private $_url; + /** * @param string $slug * @param string $scope 'app', 'developer', 'user' or 'install'. @@ -72,6 +80,7 @@ class FS_Api { * @param bool $is_sandbox * @param bool|string $secret_key Element's secret key. * @param null|string $sdk_version + * @param null|string $url * * @return FS_Api */ @@ -82,14 +91,15 @@ static function instance( $public_key, $is_sandbox, $secret_key = false, - $sdk_version = null + $sdk_version = null, + $url = null ) { $identifier = md5( $slug . $scope . $id . $public_key . ( is_string( $secret_key ) ? $secret_key : '' ) . json_encode( $is_sandbox ) ); if ( ! isset( self::$_instances[ $identifier ] ) ) { self::_init(); - self::$_instances[ $identifier ] = new FS_Api( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox, $sdk_version ); + self::$_instances[ $identifier ] = new FS_Api( $slug, $scope, $id, $public_key, $secret_key, $is_sandbox, $sdk_version, $url ); } return self::$_instances[ $identifier ]; @@ -123,6 +133,7 @@ private static function _init() { * @param bool|string $secret_key Element's secret key. * @param bool $is_sandbox * @param null|string $sdk_version + * @param null|string $url */ private function __construct( $slug, @@ -131,12 +142,14 @@ private function __construct( $public_key, $secret_key, $is_sandbox, - $sdk_version + $sdk_version, + $url ) { $this->_api = new Freemius_Api_WordPress( $scope, $id, $public_key, $secret_key, $is_sandbox ); $this->_slug = $slug; $this->_sdk_version = $sdk_version; + $this->_url = $url; $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $slug . '_api', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK ); } @@ -176,13 +189,15 @@ private function _sync_clock_diff( $diff = false ) { * @param string $path * @param string $method * @param array $params - * @param bool $retry Is in retry or first call attempt. + * @param bool $in_retry Is in retry or first call attempt. * * @return array|mixed|string|void */ - private function _call( $path, $method = 'GET', $params = array(), $retry = false ) { + private function _call( $path, $method = 'GET', $params = array(), $in_retry = false ) { $this->_logger->entrance( $method . ':' . $path ); + $force_http = ( ! $in_retry && self::$_options->get_option( 'api_force_http', false ) ); + if ( self::is_temporary_down() ) { $result = $this->get_temporary_unavailable_error(); } else { @@ -198,14 +213,28 @@ private function _call( $path, $method = 'GET', $params = array(), $retry = fals } } + /** + * @since 2.5.0 Include the site's URL, if available, in all API requests that are going through the API manager. + */ + if ( ! empty( $this->_url ) ) { + if ( false === strpos( $path, 'url=' ) && + ! isset( $params['url'] ) + ) { + $path = add_query_arg( 'url', $this->_url, $path ); + } + } + $result = $this->_api->Api( $path, $method, $params ); - if ( null !== $result && - isset( $result->error ) && - isset( $result->error->code ) && - 'request_expired' === $result->error->code + if ( + ! $in_retry && + null !== $result && + isset( $result->error ) && + isset( $result->error->code ) ) { - if ( ! $retry ) { + $retry = false; + + if ( 'request_expired' === $result->error->code ) { $diff = isset( $result->error->timestamp ) ? ( time() - strtotime( $result->error->timestamp ) ) : false; @@ -213,15 +242,35 @@ private function _call( $path, $method = 'GET', $params = array(), $retry = fals // Try to sync clock diff. if ( false !== $this->_sync_clock_diff( $diff ) ) { // Retry call with new synced clock. - return $this->_call( $path, $method, $params, true ); + $retry = true; + } + } else if ( + Freemius_Api_WordPress::IsHttps() && + FS_Api::is_ssl_error_response( $result ) + ) { + $force_http = true; + $retry = true; + } + + if ( $retry ) { + if ( $force_http ) { + $this->toggle_force_http( true ); } + + $result = $this->_call( $path, $method, $params, true ); } } } - if ( $this->_logger->is_on() && self::is_api_error( $result ) ) { - // Log API errors. - $this->_logger->api_error( $result ); + if ( self::is_api_error( $result ) ) { + if ( $this->_logger->is_on() ) { + // Log API errors. + $this->_logger->api_error( $result ); + } + + if ( $force_http ) { + $this->toggle_force_http( false ); + } } return $result; @@ -313,6 +362,43 @@ function get( $path = '/', $flush = false, $expiration = WP_FS__TIME_24_HOURS_IN return $cached_result; } + /** + * @todo Remove this method after migrating Freemius::safe_remote_post() to FS_Api::call(). + * + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param string $url + * @param array $remote_args + * + * @return array|WP_Error The response array or a WP_Error on failure. + */ + static function remote_request( $url, $remote_args ) { + if ( ! class_exists( 'Freemius_Api_WordPress' ) ) { + require_once WP_FS__DIR_SDK . '/FreemiusWordPress.php'; + } + + if ( method_exists( 'Freemius_Api_WordPress', 'RemoteRequest' ) ) { + return Freemius_Api_WordPress::RemoteRequest( $url, $remote_args ); + } + + // The following is for backward compatibility when a modified PHP SDK version is in use and the `Freemius_Api_WordPress:RemoteRequest()` method doesn't exist. + $response = wp_remote_request( $url, $remote_args ); + + if ( + is_array( $response ) && + ( + empty( $response['headers'] ) || + empty( $response['headers']['x-api-server'] ) + ) + ) { + // API is considered blocked if the response doesn't include the `x-api-server` header. When there's no error but this header doesn't exist, the response is usually not in the expected form (e.g., cannot be JSON-decoded). + $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) ); + } + + return $response; + } + /** * Check if there's a cached version of the API request. * @@ -383,50 +469,37 @@ private function get_cache_key( $path, $method = 'GET', $params = array() ) { return strtolower( $method . ':' . $canonized ) . ( ! empty( $params ) ? '#' . md5( json_encode( $params ) ) : '' ); } - /** - * Test API connectivity. - * - * @author Vova Feldman (@svovaf) - * @since 1.0.9 If fails, try to fallback to HTTP. - * @since 1.1.6 Added a 5-min caching mechanism, to prevent from overloading the server if the API if - * temporary down. - * - * @return bool True if successful connectivity to the API. - */ - static function test() { - self::_init(); - - $cache_key = 'ping_test'; - - $test = self::$_cache->get_valid( $cache_key, null ); - - if ( is_null( $test ) ) { - $test = Freemius_Api_WordPress::Test(); - - if ( false === $test && Freemius_Api_WordPress::IsHttps() ) { - // Fallback to HTTP, since HTTPS fails. - Freemius_Api_WordPress::SetHttp(); - - self::$_options->set_option( 'api_force_http', true, true ); - - $test = Freemius_Api_WordPress::Test(); - - if ( false === $test ) { - /** - * API connectivity test fail also in HTTP request, therefore, - * fallback to HTTPS to keep connection secure. - * - * @since 1.1.6 - */ - self::$_options->set_option( 'api_force_http', false, true ); - } - } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param bool $is_http + */ + private function toggle_force_http( $is_http ) { + self::$_options->set_option( 'api_force_http', $is_http, true ); - self::$_cache->set( $cache_key, $test, WP_FS__TIME_5_MIN_IN_SEC ); - } + if ( $is_http ) { + Freemius_Api_WordPress::SetHttp(); + } else if ( method_exists( 'Freemius_Api_WordPress', 'SetHttps' ) ) { + Freemius_Api_WordPress::SetHttps(); + } + } - return $test; - } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param mixed $response + * + * @return bool + */ + static function is_blocked( $response ) { + return ( + self::is_api_error_object( $response, true ) && + isset( $response->error->code ) && + 'api_blocked' === $response->error->code + ); + } /** * Check if API is temporary down. @@ -461,56 +534,6 @@ private function get_temporary_unavailable_error() { ); } - /** - * Ping API for connectivity test, and return result object. - * - * @author Vova Feldman (@svovaf) - * @since 1.0.9 - * - * @param null|string $unique_anonymous_id - * @param array $params - * - * @return object - */ - function ping( $unique_anonymous_id = null, $params = array() ) { - $this->_logger->entrance(); - - if ( self::is_temporary_down() ) { - return $this->get_temporary_unavailable_error(); - } - - $pong = is_null( $unique_anonymous_id ) ? - Freemius_Api_WordPress::Ping() : - $this->_call( 'ping.json?' . http_build_query( array_merge( - array( 'uid' => $unique_anonymous_id ), - $params - ) ) ); - - if ( $this->is_valid_ping( $pong ) ) { - return $pong; - } - - if ( self::should_try_with_http( $pong ) ) { - // Fallback to HTTP, since HTTPS fails. - Freemius_Api_WordPress::SetHttp(); - - self::$_options->set_option( 'api_force_http', true, true ); - - $pong = is_null( $unique_anonymous_id ) ? - Freemius_Api_WordPress::Ping() : - $this->_call( 'ping.json?' . http_build_query( array_merge( - array( 'uid' => $unique_anonymous_id ), - $params - ) ) ); - - if ( ! $this->is_valid_ping( $pong ) ) { - self::$_options->set_option( 'api_force_http', false, true ); - } - } - - return $pong; - } - /** * Check if based on the API result we should try * to re-run the same request with HTTP instead of HTTPS. @@ -540,20 +563,6 @@ private static function should_try_with_http( $result ) { } - /** - * Check if valid ping request result. - * - * @author Vova Feldman (@svovaf) - * @since 1.1.1 - * - * @param mixed $pong - * - * @return bool - */ - function is_valid_ping( $pong ) { - return Freemius_Api_WordPress::Test( $pong ); - } - function get_url( $path = '' ) { return Freemius_Api_WordPress::GetUrl( $path, $this->_api->IsSandbox() ); } @@ -571,6 +580,14 @@ static function clear_cache() { self::$_cache->clear(); } + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + */ + static function clear_force_http_flag() { + self::$_options->unset_option( 'api_force_http' ); + } + #---------------------------------------------------------------------------------- #region Error Handling #---------------------------------------------------------------------------------- @@ -593,14 +610,52 @@ static function is_api_error( $result ) { * @since 2.0.0 * * @param mixed $result + * @param bool $ignore_message * * @return bool Is API result contains an error. */ - static function is_api_error_object( $result ) { + static function is_api_error_object( $result, $ignore_message = false ) { return ( is_object( $result ) && isset( $result->error ) && - isset( $result->error->message ) + ( $ignore_message || isset( $result->error->message ) ) + ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.4 + * + * @param WP_Error|object|string $response + * + * @return bool + */ + static function is_ssl_error_response( $response ) { + $http_error = null; + + if ( $response instanceof WP_Error ) { + if ( + isset( $response->errors ) && + isset( $response->errors['http_request_failed'] ) + ) { + $http_error = strtolower( $response->errors['http_request_failed'][0] ); + } + } else if ( + self::is_api_error_object( $response ) && + ! empty( $response->error->message ) + ) { + $http_error = $response->error->message; + } + + return ( + ! empty( $http_error ) && + ( + false !== strpos( $http_error, 'curl error 35' ) || + ( + false === strpos( $http_error, '' ) && + false !== strpos( $http_error, 'ssl' ) + ) + ) ); } diff --git a/freemius/includes/class-fs-lock.php b/freemius/includes/class-fs-lock.php new file mode 100644 index 0000000..3c93fda --- /dev/null +++ b/freemius/includes/class-fs-lock.php @@ -0,0 +1,110 @@ +_lock_id = $lock_id; + + if ( ! isset( self::$_thread_id ) ) { + self::$_thread_id = mt_rand( 0, 32000 ); + } + } + + /** + * Try to acquire lock. If the lock is already set or is being acquired by another locker, don't do anything. + * + * @param int $expiration + * + * @return bool TRUE if successfully acquired lock. + */ + function try_lock( $expiration = 0 ) { + if ( $this->is_locked() ) { + // Already locked. + return false; + } + + set_site_transient( $this->_lock_id, self::$_thread_id, $expiration ); + + if ( $this->has_lock() ) { + $this->lock($expiration); + + return true; + } + + return false; + } + + /** + * Acquire lock regardless if it's already acquired by another locker or not. + * + * @author Vova Feldman (@svovaf) + * @since 2.1.0 + * + * @param int $expiration + */ + function lock( $expiration = 0 ) { + set_site_transient( $this->_lock_id, true, $expiration ); + } + + /** + * Checks if lock is currently acquired. + * + * @author Vova Feldman (@svovaf) + * @since 2.1.0 + * + * @return bool + */ + function is_locked() { + return ( false !== get_site_transient( $this->_lock_id ) ); + } + + /** + * Unlock the lock. + * + * @author Vova Feldman (@svovaf) + * @since 2.1.0 + */ + function unlock() { + delete_site_transient( $this->_lock_id ); + } + + /** + * Checks if lock is currently acquired by the current locker. + * + * @return bool + */ + protected function has_lock() { + return ( self::$_thread_id == get_site_transient( $this->_lock_id ) ); + } + } \ No newline at end of file diff --git a/freemius/includes/class-fs-logger.php b/freemius/includes/class-fs-logger.php index 624c683..6dde8aa 100644 --- a/freemius/includes/class-fs-logger.php +++ b/freemius/includes/class-fs-logger.php @@ -38,9 +38,10 @@ class FS_Logger { private static $_HOOKED_FOOTER = false; private function __construct( $id, $on = false, $echo = false ) { + $bt = debug_backtrace(); + $this->_id = $id; - $bt = debug_backtrace(); $caller = $bt[2]; if ( false !== strpos( $caller['file'], 'plugins' ) ) { diff --git a/freemius/includes/class-fs-plugin-updater.php b/freemius/includes/class-fs-plugin-updater.php index 8f6b996..e904a08 100644 --- a/freemius/includes/class-fs-plugin-updater.php +++ b/freemius/includes/class-fs-plugin-updater.php @@ -134,7 +134,7 @@ private function filters() { function catch_plugin_information_dialog_contents() { if ( 'plugin-information' !== fs_request_get( 'tab', false ) || - $this->_fs->get_slug() !== fs_request_get( 'plugin', false ) + $this->_fs->get_slug() !== fs_request_get_raw( 'plugin', false ) ) { return; } @@ -153,7 +153,7 @@ function catch_plugin_information_dialog_contents() { function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { if ( 'plugin-information' !== fs_request_get( 'tab', false ) || - $this->_fs->get_slug() !== fs_request_get( 'plugin', false ) + $this->_fs->get_slug() !== fs_request_get_raw( 'plugin', false ) ) { return; } @@ -166,15 +166,19 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { $contents = ob_get_clean(); - $update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' ); + $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_install_from_iframe"' ); - if ( false !== $update_button_id_attribute_pos ) { - $update_button_start_pos = strrpos( - substr( $contents, 0, $update_button_id_attribute_pos ), + if ( false === $install_or_update_button_id_attribute_pos ) { + $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' ); + } + + if ( false !== $install_or_update_button_id_attribute_pos ) { + $install_or_update_button_start_pos = strrpos( + substr( $contents, 0, $install_or_update_button_id_attribute_pos ), '', $update_button_id_attribute_pos ) + strlen( '' ) ); + $install_or_update_button_end_pos = ( strpos( $contents, '', $install_or_update_button_id_attribute_pos ) + strlen( '' ) ); /** * The part of the contents without the update button. @@ -182,20 +186,20 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { * @author Leo Fajardo (@leorw) * @since 2.2.5 */ - $modified_contents = substr( $contents, 0, $update_button_start_pos ); + $modified_contents = substr( $contents, 0, $install_or_update_button_start_pos ); - $update_button = substr( $contents, $update_button_start_pos, ( $update_button_end_pos - $update_button_start_pos ) ); + $install_or_update_button = substr( $contents, $install_or_update_button_start_pos, ( $install_or_update_button_end_pos - $install_or_update_button_start_pos ) ); /** * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license, * the text will be "Renew license" and will link to the checkout page with the license's billing cycle * and quota. If there's no license, the text will be "Buy license" and will link to the pricing page. */ - $update_button = preg_replace( - '/(\)(.+)(\<\/a>)/is', + $install_or_update_button = preg_replace( + '/(\)(.+)(\<\/a>)/is', is_object( $license ) ? sprintf( - '$1$3%s$5%s$7', + '$1$4%s$6%s$8', $this->_fs->checkout_url( is_object( $subscription ) ? ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) : @@ -206,11 +210,11 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { fs_text_inline( 'Renew license', 'renew-license', $this->_fs->get_slug() ) ) : sprintf( - '$1$3%s$5%s$7', + '$1$4%s$6%s$8', $this->_fs->pricing_url(), fs_text_inline( 'Buy license', 'buy-license', $this->_fs->get_slug() ) ), - $update_button + $install_or_update_button ); /** @@ -219,7 +223,7 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { * @author Leo Fajardo (@leorw) * @since 2.2.5 */ - $modified_contents .= $update_button; + $modified_contents .= $install_or_update_button; /** * Append the remaining part of the contents after the update button. @@ -227,7 +231,7 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { * @author Leo Fajardo (@leorw) * @since 2.2.5 */ - $modified_contents .= substr( $contents, $update_button_end_pos ); + $modified_contents .= substr( $contents, $install_or_update_button_end_pos ); $contents = $modified_contents; } @@ -240,7 +244,11 @@ function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) { * @since 2.0.0 */ private function add_transient_filters() { - if ( $this->_fs->is_premium() && ! $this->_fs->is_tracking_allowed() ) { + if ( + $this->_fs->is_premium() && + $this->_fs->is_registered() && + ! FS_Permission_Manager::instance( $this->_fs )->is_essentials_tracking_allowed() + ) { $this->_logger->log( 'Opted out sites cannot receive automatic software updates.' ); return; @@ -413,7 +421,7 @@ function change_theme_update_info_html( $prepared_themes ) { $themes_update = get_site_transient( 'update_themes' ); if ( ! isset( $themes_update->response[ $theme_basename ] ) || - empty( $themes_update->response[ $theme_basename ]['package'] ) + empty( $themes_update->response[ $theme_basename ]['package'] ) ) { return $prepared_themes; } @@ -610,11 +618,9 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) { if ( ! isset( $this->_translation_updates ) ) { $this->_translation_updates = array(); - if ( current_user_can( 'update_languages' ) ) { - $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug ); - if ( ! empty( $translation_updates ) ) { - $this->_translation_updates = $translation_updates; - } + $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug ); + if ( ! empty( $translation_updates ) ) { + $this->_translation_updates = $translation_updates; } } @@ -634,7 +640,7 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) { foreach ( $this->_translation_updates as $translation_update ) { $lang = $translation_update['language']; if ( ! isset( $current_plugin_translation_updates_map[ $lang ] ) || - version_compare( $translation_update['version'], $current_plugin_translation_updates_map[ $lang ]['version'], '>' ) + version_compare( $translation_update['version'], $current_plugin_translation_updates_map[ $lang ]['version'], '>' ) ) { $current_plugin_translation_updates_map[ $lang ] = $translation_update; } @@ -648,7 +654,7 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) { } /** - * Get module's required data for the updates mechanism. + * Get module's required data for the updates' mechanism. * * @author Vova Feldman (@svovaf) * @since 2.0.0 @@ -658,13 +664,14 @@ function pre_set_site_transient_update_plugins_filter( $transient_data ) { * @return object */ function get_update_details( FS_Plugin_Tag $new_version ) { - $update = new stdClass(); - $update->slug = $this->_fs->get_slug(); - $update->new_version = $new_version->version; - $update->url = WP_FS__ADDRESS; - $update->package = $new_version->url; - $update->tested = $new_version->tested_up_to_version; - $update->requires = $new_version->requires_platform_version; + $update = new stdClass(); + $update->slug = $this->_fs->get_slug(); + $update->new_version = $new_version->version; + $update->url = WP_FS__ADDRESS; + $update->package = $new_version->url; + $update->tested = self::get_tested_wp_version( $new_version->tested_up_to_version ); + $update->requires = $new_version->requires_platform_version; + $update->requires_php = $new_version->requires_programming_language_version; $icon = $this->_fs->get_local_icon_url(); @@ -806,9 +813,9 @@ function delete_update_data() { $basename = $this->_fs->get_plugin_basename(); if ( ! is_object( $transient_data ) || - ! isset( $transient_data->response ) || + ! isset( $transient_data->response ) || ! is_array( $transient_data->response ) || - empty( $transient_data->response[ $basename ] ) + empty( $transient_data->response[ $basename ] ) ) { return; } @@ -835,28 +842,34 @@ function delete_update_data() { * @return bool|mixed */ static function _fetch_plugin_info_from_repository( $action, $args ) { - $url = $http_url = 'http://api.wordpress.org/plugins/info/1.0/'; - if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) { - $url = set_url_scheme( $url, 'https' ); - } - - $args = array( - 'timeout' => 15, - 'body' => array( + $url = $http_url = 'http://api.wordpress.org/plugins/info/1.2/'; + $url = add_query_arg( + array( 'action' => $action, - 'request' => serialize( $args ) - ) + 'request' => $args, + ), + $url ); - $request = wp_remote_post( $url, $args ); + if ( wp_http_supports( array( 'ssl' ) ) ) { + $url = set_url_scheme( $url, 'https' ); + } + + // The new endpoint version serves only GET requests. + $request = wp_remote_get( $url, array( 'timeout' => 15 ) ); if ( is_wp_error( $request ) ) { return false; } - $res = maybe_unserialize( wp_remote_retrieve_body( $request ) ); + $res = json_decode( wp_remote_retrieve_body( $request ), true ); + + if ( is_array( $res ) ) { + // Object casting is required in order to match the info/1.0 format. We are not decoding directly into an object as we need some fields to remain an array (e.g., $res->sections). + $res = (object) $res; + } - if ( ! is_object( $res ) && ! is_array( $res ) ) { + if ( ! is_object( $res ) || isset( $res->error ) ) { return false; } @@ -1093,6 +1106,7 @@ function plugins_api_filter( $data, $action = '', $args = null ) { if ( ! $plugin_in_repo ) { $data->last_updated = ! is_null( $new_version->updated ) ? $new_version->updated : $new_version->created; $data->requires = $new_version->requires_platform_version; + $data->requires_php = $new_version->requires_programming_language_version; $data->tested = $new_version->tested_up_to_version; } @@ -1146,9 +1160,28 @@ function plugins_api_filter( $data, $action = '', $args = null ) { } } + if ( ! empty( $data->tested ) ) { + $data->tested = self::get_tested_wp_version( $data->tested ); + } + return $data; } + /** + * @since 2.5.3 If the current WordPress version is a patch of the tested version (e.g., 6.1.2 is a patch of 6.1), then override the tested version with the patch so developers won't need to release a new version just to bump the latest supported WP version. + * + * @param string|null $tested_up_to + * + * @return string|null + */ + private static function get_tested_wp_version( $tested_up_to ) { + $current_wp_version = get_bloginfo( 'version' ); + + return ( ! empty($tested_up_to) && fs_starts_with( $current_wp_version, $tested_up_to . '.' ) ) ? + $current_wp_version : + $tested_up_to; + } + /** * @author Vova Feldman (@svovaf) * @since 1.2.1.7 diff --git a/freemius/includes/class-fs-storage.php b/freemius/includes/class-fs-storage.php index 187a011..c89f473 100644 --- a/freemius/includes/class-fs-storage.php +++ b/freemius/includes/class-fs-storage.php @@ -15,9 +15,11 @@ * * A wrapper class for handling network level and single site level storage. * - * @property bool $is_network_activation - * @property int $network_install_blog_id - * @property object $sync_cron + * @property bool $is_network_activation + * @property int $network_install_blog_id + * @property bool|null $is_extensions_tracking_allowed + * @property bool|null $is_diagnostic_tracking_allowed + * @property object $sync_cron */ class FS_Storage { /** @@ -72,6 +74,16 @@ class FS_Storage { */ private static $_NETWORK_OPTIONS_MAP; + const OPTION_LEVEL_UNDEFINED = -1; + // The option should be stored on the network level. + const OPTION_LEVEL_NETWORK = 0; + // The option should be stored on the network level when the plugin is network-activated. + const OPTION_LEVEL_NETWORK_ACTIVATED = 1; + // The option should be stored on the network level when the plugin is network-activated and the opt-in connection was NOT delegated to the sub-site admin. + const OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED = 2; + // The option should be stored on the site level. + const OPTION_LEVEL_SITE = 3; + /** * @author Leo Fajardo (@leorw) * @@ -142,10 +154,17 @@ function set_site_blog_context( $blog_id ) { * @param string $key * @param mixed $value * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_BINARY_MAP). + * @param int $option_level Since 2.5.1 * @param bool $flush */ - function store( $key, $value, $network_level_or_blog_id = null, $flush = true ) { - if ( $this->should_use_network_storage( $key, $network_level_or_blog_id ) ) { + function store( + $key, + $value, + $network_level_or_blog_id = null, + $option_level = self::OPTION_LEVEL_UNDEFINED, + $flush = true + ) { + if ( $this->should_use_network_storage( $key, $network_level_or_blog_id, $option_level ) ) { $this->_network_storage->store( $key, $value, $flush ); } else { $storage = $this->get_site_storage( $network_level_or_blog_id ); @@ -199,11 +218,17 @@ function remove( $key, $store = true, $network_level_or_blog_id = null ) { * @param string $key * @param mixed $default * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_BINARY_MAP). + * @param int $option_level Since 2.5.1 * * @return mixed */ - function get( $key, $default = false, $network_level_or_blog_id = null ) { - if ( $this->should_use_network_storage( $key, $network_level_or_blog_id ) ) { + function get( + $key, + $default = false, + $network_level_or_blog_id = null, + $option_level = self::OPTION_LEVEL_UNDEFINED + ) { + if ( $this->should_use_network_storage( $key, $network_level_or_blog_id, $option_level ) ) { return $this->_network_storage->get( $key, $default ); } else { $storage = $this->get_site_storage( $network_level_or_blog_id ); @@ -289,19 +314,6 @@ function migrate_to_network() { // Migrate option to the network storage. $this->_network_storage->store( $option, $this->_storage->{$option}, false ); - /** - * Remove the option from site level storage. - * - * IMPORTANT: - * The line below is intentionally commented since we want to preserve the option - * on the site storage level for "downgrade compatibility". Basically, if the user - * will downgrade to an older version of the plugin with the prev storage structure, - * it will continue working. - * - * @todo After a few releases we can remove this. - */ -// $this->_storage->remove($option, false); - $updated = true; } } @@ -336,63 +348,61 @@ function migrate_to_network() { private static function load_network_options_map() { self::$_NETWORK_OPTIONS_MAP = array( // Network level options. - 'affiliate_application_data' => 0, - 'beta_data' => 0, - 'connectivity_test' => 0, - 'handle_gdpr_admin_notice' => 0, - 'has_trial_plan' => 0, - 'install_sync_timestamp' => 0, - 'install_sync_cron' => 0, - 'is_anonymous_ms' => 0, - 'is_network_activated' => 0, - 'is_on' => 0, - 'is_plugin_new_install' => 0, - 'network_install_blog_id' => 0, - 'pending_sites_info' => 0, - 'plugin_last_version' => 0, - 'plugin_main_file' => 0, - 'plugin_version' => 0, - 'sdk_downgrade_mode' => 0, - 'sdk_last_version' => 0, - 'sdk_upgrade_mode' => 0, - 'sdk_version' => 0, - 'sticky_optin_added_ms' => 0, - 'subscriptions' => 0, - 'sync_timestamp' => 0, - 'sync_cron' => 0, - 'was_plugin_loaded' => 0, - 'network_user_id' => 0, - 'plugin_upgrade_mode' => 0, - 'plugin_downgrade_mode' => 0, - 'is_network_connected' => 0, + 'affiliate_application_data' => self::OPTION_LEVEL_NETWORK, + 'beta_data' => self::OPTION_LEVEL_NETWORK, + 'connectivity_test' => self::OPTION_LEVEL_NETWORK, + 'handle_gdpr_admin_notice' => self::OPTION_LEVEL_NETWORK, + 'has_trial_plan' => self::OPTION_LEVEL_NETWORK, + 'install_sync_timestamp' => self::OPTION_LEVEL_NETWORK, + 'install_sync_cron' => self::OPTION_LEVEL_NETWORK, + 'is_anonymous_ms' => self::OPTION_LEVEL_NETWORK, + 'is_network_activated' => self::OPTION_LEVEL_NETWORK, + 'is_on' => self::OPTION_LEVEL_NETWORK, + 'is_plugin_new_install' => self::OPTION_LEVEL_NETWORK, + 'network_install_blog_id' => self::OPTION_LEVEL_NETWORK, + 'pending_sites_info' => self::OPTION_LEVEL_NETWORK, + 'plugin_last_version' => self::OPTION_LEVEL_NETWORK, + 'plugin_main_file' => self::OPTION_LEVEL_NETWORK, + 'plugin_version' => self::OPTION_LEVEL_NETWORK, + 'sdk_downgrade_mode' => self::OPTION_LEVEL_NETWORK, + 'sdk_last_version' => self::OPTION_LEVEL_NETWORK, + 'sdk_upgrade_mode' => self::OPTION_LEVEL_NETWORK, + 'sdk_version' => self::OPTION_LEVEL_NETWORK, + 'sticky_optin_added_ms' => self::OPTION_LEVEL_NETWORK, + 'subscriptions' => self::OPTION_LEVEL_NETWORK, + 'sync_timestamp' => self::OPTION_LEVEL_NETWORK, + 'sync_cron' => self::OPTION_LEVEL_NETWORK, + 'was_plugin_loaded' => self::OPTION_LEVEL_NETWORK, + 'network_user_id' => self::OPTION_LEVEL_NETWORK, + 'plugin_upgrade_mode' => self::OPTION_LEVEL_NETWORK, + 'plugin_downgrade_mode' => self::OPTION_LEVEL_NETWORK, + 'is_network_connected' => self::OPTION_LEVEL_NETWORK, /** - * Special flag that is used when a super-admin upgrades to the new version of the SDK that - * supports network level integration, when the connection decision wasn't made for all of the - * sites in the network. + * Special flag that is used when a super-admin upgrades to the new version of the SDK that supports network level integration, when the connection decision wasn't made for all the sites in the network. */ - 'is_network_activation' => 0, - 'license_migration' => 0, + 'is_network_activation' => self::OPTION_LEVEL_NETWORK, + 'license_migration' => self::OPTION_LEVEL_NETWORK, // When network activated, then network level. - 'install_timestamp' => 1, - 'prev_is_premium' => 1, - 'require_license_activation' => 1, + 'install_timestamp' => self::OPTION_LEVEL_NETWORK_ACTIVATED, + 'prev_is_premium' => self::OPTION_LEVEL_NETWORK_ACTIVATED, + 'require_license_activation' => self::OPTION_LEVEL_NETWORK_ACTIVATED, // If not network activated OR delegated, then site level. - 'activation_timestamp' => 2, - 'expired_license_notice_shown' => 2, - 'is_whitelabeled' => 2, - 'last_license_key' => 2, - 'last_license_user_id' => 2, - 'prev_user_id' => 2, - 'sticky_optin_added' => 2, - 'uninstall_reason' => 2, - 'is_pending_activation' => 2, - 'pending_license_key' => 2, - 'is_extensions_tracking_allowed' => 2, + 'activation_timestamp' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'expired_license_notice_shown' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'is_whitelabeled' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'last_license_key' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'last_license_user_id' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'prev_user_id' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'sticky_optin_added' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'uninstall_reason' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'is_pending_activation' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, + 'pending_license_key' => self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED, // Site level options. - 'is_anonymous' => 3, + 'is_anonymous' => self::OPTION_LEVEL_SITE, + 'clone_id' => self::OPTION_LEVEL_SITE, ); } @@ -403,25 +413,33 @@ private static function load_network_options_map() { * @since 2.0.0 * * @param string $key + * @param int $option_level Since 2.5.1 * - * @return bool|mixed + * @return bool */ - private function is_multisite_option( $key ) { + private function is_multisite_option( $key, $option_level = self::OPTION_LEVEL_UNDEFINED ) { if ( ! isset( self::$_NETWORK_OPTIONS_MAP ) ) { self::load_network_options_map(); } - if ( ! isset( self::$_NETWORK_OPTIONS_MAP[ $key ] ) ) { + if ( + self::OPTION_LEVEL_UNDEFINED === $option_level && + isset( self::$_NETWORK_OPTIONS_MAP[ $key ] ) + ) { + $option_level = self::$_NETWORK_OPTIONS_MAP[ $key ]; + } + + if ( self::OPTION_LEVEL_UNDEFINED === $option_level ) { // Option not found -> use site level storage. return false; } - if ( 0 === self::$_NETWORK_OPTIONS_MAP[ $key ] ) { + if ( self::OPTION_LEVEL_NETWORK === $option_level ) { // Option found and set to always use the network level storage on a multisite. return true; } - if ( 3 === self::$_NETWORK_OPTIONS_MAP[ $key ] ) { + if ( self::OPTION_LEVEL_SITE === $option_level ) { // Option found and set to always use the site level storage on a multisite. return false; } @@ -430,12 +448,15 @@ private function is_multisite_option( $key ) { return false; } - if ( 1 === self::$_NETWORK_OPTIONS_MAP[ $key ] ) { + if ( self::OPTION_LEVEL_NETWORK_ACTIVATED === $option_level ) { // Network activated. return true; } - if ( 2 === self::$_NETWORK_OPTIONS_MAP[ $key ] && ! $this->_is_delegated_connection ) { + if ( + self::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED === $option_level && + ! $this->_is_delegated_connection + ) { // Network activated and not delegated. return true; } @@ -448,10 +469,15 @@ private function is_multisite_option( $key ) { * * @param string $key * @param null|bool|int $network_level_or_blog_id When an integer, use the given blog storage. When `true` use the multisite storage (if there's a network). When `false`, use the current context blog storage. When `null`, the decision which storage to use (MS vs. Current S) will be handled internally and determined based on the $option (based on self::$_BINARY_MAP). + * @param int $option_level Since 2.5.1 * * @return bool */ - private function should_use_network_storage( $key, $network_level_or_blog_id = null ) { + private function should_use_network_storage( + $key, + $network_level_or_blog_id = null, + $option_level = self::OPTION_LEVEL_UNDEFINED + ) { if ( ! $this->_is_multisite ) { // Not a multisite environment. return false; @@ -463,12 +489,12 @@ private function should_use_network_storage( $key, $network_level_or_blog_id = n } if ( is_bool( $network_level_or_blog_id ) ) { - // Explicitly specified whether should use the network or blog level storage. + // Explicitly specified whether it should use the network or blog level storage. return $network_level_or_blog_id; } // Determine which storage to use based on the option. - return $this->is_multisite_option( $key ); + return $this->is_multisite_option( $key, $option_level ); } /** @@ -529,4 +555,4 @@ function __get( $k ) { } #endregion - } \ No newline at end of file + } diff --git a/freemius/includes/class-fs-user-lock.php b/freemius/includes/class-fs-user-lock.php index 842cbba..32f6079 100644 --- a/freemius/includes/class-fs-user-lock.php +++ b/freemius/includes/class-fs-user-lock.php @@ -10,18 +10,16 @@ exit; } + require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php'; + /** * Class FS_User_Lock */ class FS_User_Lock { /** - * @var int - */ - private $_wp_user_id; - /** - * @var int + * @var FS_Lock */ - private $_thread_id; + private $_lock; #-------------------------------------------------------------------------------- #region Singleton @@ -49,10 +47,10 @@ static function instance() { #endregion private function __construct() { - $this->_wp_user_id = Freemius::get_current_wp_user_id(); - $this->_thread_id = mt_rand( 0, 32000 ); - } + $current_user_id = Freemius::get_current_wp_user_id(); + $this->_lock = new FS_Lock( "locked_{$current_user_id}" ); + } /** * Try to acquire lock. If the lock is already set or is being acquired by another locker, don't do anything. @@ -65,20 +63,7 @@ private function __construct() { * @return bool TRUE if successfully acquired lock. */ function try_lock( $expiration = 0 ) { - if ( $this->is_locked() ) { - // Already locked. - return false; - } - - set_site_transient( "locked_{$this->_wp_user_id}", $this->_thread_id, $expiration ); - - if ( $this->has_lock() ) { - set_site_transient( "locked_{$this->_wp_user_id}", true, $expiration ); - - return true; - } - - return false; + return $this->_lock->try_lock( $expiration ); } /** @@ -90,19 +75,7 @@ function try_lock( $expiration = 0 ) { * @param int $expiration */ function lock( $expiration = 0 ) { - set_site_transient( "locked_{$this->_wp_user_id}", true, $expiration ); - } - - /** - * Checks if lock is currently acquired. - * - * @author Vova Feldman (@svovaf) - * @since 2.1.0 - * - * @return bool - */ - function is_locked() { - return ( false !== get_site_transient( "locked_{$this->_wp_user_id}" ) ); + $this->_lock->lock( $expiration ); } /** @@ -112,15 +85,6 @@ function is_locked() { * @since 2.1.0 */ function unlock() { - delete_site_transient( "locked_{$this->_wp_user_id}" ); - } - - /** - * Checks if lock is currently acquired by the current locker. - * - * @return bool - */ - private function has_lock() { - return ( $this->_thread_id == get_site_transient( "locked_{$this->_wp_user_id}" ) ); + $this->_lock->unlock(); } } \ No newline at end of file diff --git a/freemius/includes/customizer/class-fs-customizer-upsell-control.php b/freemius/includes/customizer/class-fs-customizer-upsell-control.php index 94dc430..ae8d312 100644 --- a/freemius/includes/customizer/class-fs-customizer-upsell-control.php +++ b/freemius/includes/customizer/class-fs-customizer-upsell-control.php @@ -101,11 +101,11 @@ public function to_json() { } } } + + $this->json['plans'] = $pricing->plans; } } - $this->json['plans'] = $pricing->plans; - $this->json['strings'] = array( 'plan' => $this->fs->get_text_x_inline( 'Plan', 'as product pricing plan', 'plan' ), ); diff --git a/freemius/includes/entities/class-fs-affiliate-terms.php b/freemius/includes/entities/class-fs-affiliate-terms.php index 6dab87f..b4573a3 100644 --- a/freemius/includes/entities/class-fs-affiliate-terms.php +++ b/freemius/includes/entities/class-fs-affiliate-terms.php @@ -84,6 +84,10 @@ class FS_AffiliateTerms extends FS_Scope_Entity { * @var bool If `true`, allow referrals from any site. */ public $is_any_site_allowed; + /** + * @var string $plugin_title Title of the plugin. This is used in case we are showing affiliate form for a Bundle instead of the `plugin` in context. + */ + public $plugin_title; #endregion Properties diff --git a/freemius/includes/entities/class-fs-plugin-tag.php b/freemius/includes/entities/class-fs-plugin-tag.php index 739e9c8..a583a26 100644 --- a/freemius/includes/entities/class-fs-plugin-tag.php +++ b/freemius/includes/entities/class-fs-plugin-tag.php @@ -23,6 +23,10 @@ class FS_Plugin_Tag extends FS_Entity { * @var string */ public $requires_platform_version; + /** + * @var string + */ + public $requires_programming_language_version; /** * @var string */ diff --git a/freemius/includes/entities/class-fs-plugin.php b/freemius/includes/entities/class-fs-plugin.php index 2bc039a..ad5a3de 100644 --- a/freemius/includes/entities/class-fs-plugin.php +++ b/freemius/includes/entities/class-fs-plugin.php @@ -104,6 +104,11 @@ class FS_Plugin extends FS_Scope_Entity { * @var null|string */ public $bundle_public_key; + /** + * @since 2.5.4 + * @var null|array + */ + public $opt_in_moderation; const AFFILIATE_MODERATION_CUSTOMERS = 'customers'; diff --git a/freemius/includes/entities/class-fs-site.php b/freemius/includes/entities/class-fs-site.php index 984d8f9..19cca04 100644 --- a/freemius/includes/entities/class-fs-site.php +++ b/freemius/includes/entities/class-fs-site.php @@ -10,6 +10,9 @@ exit; } + /** + * @property int $blog_id + */ class FS_Site extends FS_Scope_Entity { /** * @var number @@ -39,10 +42,6 @@ class FS_Site extends FS_Scope_Entity { * @var string E.g. en-GB */ public $language; - /** - * @var string E.g. UTF-8 - */ - public $charset; /** * @var string Platform version (e.g WordPress version). */ @@ -86,6 +85,8 @@ class FS_Site extends FS_Scope_Entity { * @author Leo Fajardo (@leorw) * * @since 1.2.1.5 + * @deprecated Since 2.5.1 + * @todo Remove after a few releases. * * @var bool */ @@ -186,11 +187,20 @@ static function is_localhost_by_address( $url ) { // Cloudways fs_ends_with( $subdomain, '.cloudwaysapps.com' ) || // Kinsta - ( fs_starts_with( $subdomain, 'staging-' ) && ( fs_ends_with( $subdomain, '.kinsta.com' ) || fs_ends_with( $subdomain, '.kinsta.cloud' ) ) ) || + ( + ( fs_starts_with( $subdomain, 'staging-' ) || fs_starts_with( $subdomain, 'env-' ) ) && + ( fs_ends_with( $subdomain, '.kinsta.com' ) || fs_ends_with( $subdomain, '.kinsta.cloud' ) ) + ) || // DesktopServer fs_ends_with( $subdomain, '.dev.cc' ) || // Pressable - fs_ends_with( $subdomain, '.mystagingwebsite.com' ) + fs_ends_with( $subdomain, '.mystagingwebsite.com' ) || + // WPMU DEV + ( fs_ends_with( $subdomain, '.tempurl.host' ) || fs_ends_with( $subdomain, '.wpmudev.host' ) ) || + // Vendasta + ( fs_ends_with( $subdomain, '.websitepro-staging.com' ) || fs_ends_with( $subdomain, '.websitepro.hosting' ) ) || + // InstaWP + fs_ends_with( $subdomain, '.instawp.xyz' ) ); } @@ -223,31 +233,25 @@ function is_trial_utilized() { } /** - * @author Vova Feldman (@svovaf) - * @since 2.0.0 + * @author Edgar Melkonyan * * @return bool */ - function is_tracking_allowed() { - return ( true !== $this->is_disconnected ); + function is_beta() { + return ( isset( $this->is_beta ) && true === $this->is_beta ); } /** - * @author Vova Feldman (@svovaf) - * @since 2.0.0 + * @author Leo Fajardo (@leorw) + * @since 2.5.1 * - * @return bool - */ - function is_tracking_prohibited() { - return ! $this->is_tracking_allowed(); - } - - /** - * @author Edgar Melkonyan + * @param string $site_url * * @return bool */ - function is_beta() { - return ( isset( $this->is_beta ) && true === $this->is_beta ); + function is_clone( $site_url ) { + $clone_install_url = trailingslashit( fs_strip_url_protocol( $this->url ) ); + + return ( $clone_install_url !== $site_url ); } } \ No newline at end of file diff --git a/freemius/includes/entities/class-fs-user.php b/freemius/includes/entities/class-fs-user.php index a329e87..a62cfe1 100644 --- a/freemius/includes/entities/class-fs-user.php +++ b/freemius/includes/entities/class-fs-user.php @@ -56,7 +56,18 @@ function is_verified() { return ( isset( $this->is_verified ) && true === $this->is_verified ); } - static function get_type() { + /** + * @author Leo Fajardo (@leorw) + * @since 2.4.2 + * + * @return bool + */ + function is_beta() { + // Return `false` since this is just for backward compatibility. + return false; + } + + static function get_type() { return 'user'; } } \ No newline at end of file diff --git a/freemius/includes/fs-core-functions.php b/freemius/includes/fs-core-functions.php index 0eafb68..3dddb40 100644 --- a/freemius/includes/fs-core-functions.php +++ b/freemius/includes/fs-core-functions.php @@ -133,46 +133,101 @@ function fs_img_url( $path, $img_dir = WP_FS__DIR_IMG ) { #region Request handlers. #-------------------------------------------------------------------------------- - if ( ! function_exists( 'fs_request_get' ) ) { + if ( ! function_exists( 'fs_request_get_raw' ) ) { /** - * A helper method to fetch GET/POST user input with an optional default value when the input is not set. - * @author Vova Feldman (@svovaf) + * A helper function to fetch GET/POST user input with an optional default value when the input is not set. + * This function does not do sanitization. It is up to the caller to properly sanitize and validate the input. + * + * The return of this function is always unslashed. + * + * @since 2.5.10 * * @param string $key * @param mixed $def - * @param string|bool $type Since 1.2.1.7 - when set to 'get' will look for the value passed via querystring, when - * set to 'post' will look for the value passed via the POST request's body, otherwise, - * will check if the parameter was passed in any of the two. + * @param string|bool $type When set to 'get', it will look for the value passed via query string. When + * set to 'post', it will look for the value passed via the POST request's body. Otherwise, + * it will check if the parameter was passed using any of the mentioned two methods. * * @return mixed */ - function fs_request_get( $key, $def = false, $type = false ) { + function fs_request_get_raw( $key, $def = false, $type = false ) { if ( is_string( $type ) ) { $type = strtolower( $type ); } /** - * Note to WordPress.org Reviewers: - * This is a helper method to fetch GET/POST user input with an optional default value when the input is not set. The actual sanitization is done in the scope of the function's usage. + * Note to WordPress.org reviewers: + * This is a helper function to fetch GET/POST user input with an optional default value when the input is not set. The actual sanitization is done in the scope of the function's usage. */ switch ( $type ) { case 'post': + // phpcs:ignore WordPress.Security.NonceVerification.Missing $value = isset( $_POST[ $key ] ) ? $_POST[ $key ] : $def; break; case 'get': + // phpcs:ignore WordPress.Security.NonceVerification.Recommended $value = isset( $_GET[ $key ] ) ? $_GET[ $key ] : $def; break; default: + // phpcs:ignore WordPress.Security.NonceVerification.Recommended $value = isset( $_REQUEST[ $key ] ) ? $_REQUEST[ $key ] : $def; break; } - return $value; + // Don't unslash if the value itself is empty (empty string, null, empty array etc). + return empty( $value ) ? $value : wp_unslash( $value ); + } + } + + if ( ! function_exists( 'fs_sanitize_input' ) ) { + /** + * Sanitizes input recursively (if an array). + * + * @param mixed $input + * + * @return mixed + * @uses sanitize_text_field() + * @since 2.5.10 + */ + function fs_sanitize_input( $input ) { + if ( is_array( $input ) ) { + foreach ( $input as $key => $value ) { + $input[ $key ] = fs_sanitize_input( $value ); + } + } else { + // Allow empty values to pass through as-is, like `null`, `''`, `0`, `'0'` etc. + $input = empty( $input ) ? $input : sanitize_text_field( $input ); + } + + return $input; + } + } + + if ( ! function_exists( 'fs_request_get' ) ) { + /** + * A helper method to fetch GET/POST user input with an optional default value when the input is not set. + * + * @author Vova Feldman (@svovaf) + * + * @note The return value is always sanitized with sanitize_text_field(). + * + * @param string $key + * @param mixed $def + * @param string|bool $type Since 1.2.1.7 - when set to 'get' will look for the value passed via querystring, when + * set to 'post' will look for the value passed via the POST request's body, otherwise, + * will check if the parameter was passed in any of the two. + * + * + * @return mixed + */ + function fs_request_get( $key, $def = false, $type = false ) { + return fs_sanitize_input( fs_request_get_raw( $key, $def, $type ) ); } } if ( ! function_exists( 'fs_request_has' ) ) { function fs_request_has( $key ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended return isset( $_REQUEST[ $key ] ); } } @@ -231,6 +286,7 @@ function fs_request_is_get() { if ( ! function_exists( 'fs_get_action' ) ) { function fs_get_action( $action_key = 'action' ) { + // phpcs:disable WordPress.Security.NonceVerification.Recommended if ( ! empty( $_REQUEST[ $action_key ] ) && is_string( $_REQUEST[ $action_key ] ) ) { return strtolower( $_REQUEST[ $action_key ] ); } @@ -244,6 +300,7 @@ function fs_get_action( $action_key = 'action' ) { } return false; + // phpcs:enable WordPress.Security.NonceVerification.Recommended } } @@ -756,7 +813,7 @@ function fs_sort_by_priority( $a, $b ) { } // If b has a priority and a does not, b wins. elseif ( isset( $a['priority'] ) && ! isset( $b['priority'] ) ) { return - 1; - } // If neither has a priority or both priorities are equal its a tie. + } // If neither has a priority or both priorities are equal it's a tie. elseif ( ( ! isset( $a['priority'] ) && ! isset( $b['priority'] ) ) || $a['priority'] === $b['priority'] ) { return 0; } @@ -770,6 +827,12 @@ function fs_sort_by_priority( $a, $b ) { #region Localization #-------------------------------------------------------------------------------- + global $fs_text_overrides; + + if ( ! isset( $fs_text_overrides ) ) { + $fs_text_overrides = array(); + } + if ( ! function_exists( 'fs_text' ) ) { /** * Retrieve a translated text by key. @@ -782,12 +845,10 @@ function fs_sort_by_priority( $a, $b ) { * * @return string * - * @global $fs_text , $fs_text_overrides + * @global $fs_text_overrides */ function fs_text( $key, $slug = 'freemius' ) { - global $fs_text, - $fs_module_info_text, - $fs_text_overrides; + global $fs_text_overrides; if ( isset( $fs_text_overrides[ $slug ] ) ) { if ( isset( $fs_text_overrides[ $slug ][ $key ] ) ) { @@ -800,22 +861,6 @@ function fs_text( $key, $slug = 'freemius' ) { } } - if ( ! isset( $fs_text ) ) { - $dir = defined( 'WP_FS__DIR_INCLUDES' ) ? - WP_FS__DIR_INCLUDES : - dirname( __FILE__ ); - - require_once $dir . '/i18n.php'; - } - - if ( isset( $fs_text[ $key ] ) ) { - return $fs_text[ $key ]; - } - - if ( isset( $fs_module_info_text[ $key ] ) ) { - return $fs_module_info_text[ $key ]; - } - return $key; } @@ -1349,7 +1394,7 @@ function fs_override_i18n( array $key_value, $slug = 'freemius' ) { function fs_is_plugin_uninstall() { return ( defined( 'WP_UNINSTALL_PLUGIN' ) || - ( 0 < did_action( 'update_option_uninstall_plugins' ) ) + ( 0 < did_action( 'pre_uninstall_plugin' ) ) ); } } diff --git a/freemius/includes/fs-essential-functions.php b/freemius/includes/fs-essential-functions.php index 76a3ef8..c744306 100644 --- a/freemius/includes/fs-essential-functions.php +++ b/freemius/includes/fs-essential-functions.php @@ -149,111 +149,21 @@ function fs_kses_no_null( $string ) { #endregion Core Redirect (copied from BuddyPress) ----------------------------------------- - if ( ! function_exists( '__fs' ) ) { - global $fs_text_overrides; - - if ( ! isset( $fs_text_overrides ) ) { - $fs_text_overrides = array(); - } - - /** - * Retrieve a translated text by key. - * - * @deprecated Use `fs_text()` instead since methods starting with `__` trigger warnings in Php 7. - * @todo Remove this method in the future. - * - * @author Vova Feldman (@svovaf) - * @since 1.1.4 - * - * @param string $key - * @param string $slug - * - * @return string - * - * @global $fs_text, $fs_text_overrides - */ - function __fs( $key, $slug = 'freemius' ) { - _deprecated_function( __FUNCTION__, '2.0.0', 'fs_text()' ); - - global $fs_text, - $fs_module_info_text, - $fs_text_overrides; - - if ( isset( $fs_text_overrides[ $slug ] ) ) { - if ( isset( $fs_text_overrides[ $slug ][ $key ] ) ) { - return $fs_text_overrides[ $slug ][ $key ]; - } - - $lower_key = strtolower( $key ); - if ( isset( $fs_text_overrides[ $slug ][ $lower_key ] ) ) { - return $fs_text_overrides[ $slug ][ $lower_key ]; - } - } - - if ( ! isset( $fs_text ) ) { - $dir = defined( 'WP_FS__DIR_INCLUDES' ) ? - WP_FS__DIR_INCLUDES : - dirname( __FILE__ ); - - require_once $dir . '/i18n.php'; - } - - if ( isset( $fs_text[ $key ] ) ) { - return $fs_text[ $key ]; - } - - if ( isset( $fs_module_info_text[ $key ] ) ) { - return $fs_module_info_text[ $key ]; - } - - return $key; - } - - /** - * Output a translated text by key. - * - * @deprecated Use `fs_echo()` instead for consistency with `fs_text()`. - * - * @todo Remove this method in the future. - * - * @author Vova Feldman (@svovaf) - * @since 1.1.4 - * - * @param string $key - * @param string $slug - */ - function _efs( $key, $slug = 'freemius' ) { - fs_echo( $key, $slug ); - } - } - if ( ! function_exists( 'fs_get_ip' ) ) { /** - * Get client IP. - * + * Get server IP. + * + * @since 2.5.1 This method returns the server IP. + * * @author Vova Feldman (@svovaf) * @since 1.1.2 * * @return string|null */ function fs_get_ip() { - $fields = array( - 'HTTP_CF_CONNECTING_IP', - 'HTTP_CLIENT_IP', - 'HTTP_X_FORWARDED_FOR', - 'HTTP_X_FORWARDED', - 'HTTP_FORWARDED_FOR', - 'HTTP_FORWARDED', - 'REMOTE_ADDR', - ); - - foreach ( $fields as $ip_field ) { - if ( ! empty( $_SERVER[ $ip_field ] ) ) { - return $_SERVER[ $ip_field ]; - } - } - - return null; + return empty( $_SERVER[ 'SERVER_ADDR' ] ) ? + null : + $_SERVER[ 'SERVER_ADDR' ]; } } diff --git a/freemius/includes/fs-html-escaping-functions.php b/freemius/includes/fs-html-escaping-functions.php new file mode 100644 index 0000000..29f30da --- /dev/null +++ b/freemius/includes/fs-html-escaping-functions.php @@ -0,0 +1,124 @@ + true, + 'style' => true, + 'data-*' => true, + ); + + return array( + 'a' => array_merge( + $common_attributes, + array( + 'href' => true, + 'title' => true, + 'target' => true, + 'rel' => true, + ) + ), + 'img' => array_merge( + $common_attributes, + array( + 'src' => true, + 'alt' => true, + 'title' => true, + 'width' => true, + 'height' => true, + ) + ), + 'br' => $common_attributes, + 'em' => $common_attributes, + 'small' => $common_attributes, + 'strong' => $common_attributes, + 'u' => $common_attributes, + 'b' => $common_attributes, + 'hr' => $common_attributes, + 'span' => $common_attributes, + 'p' => $common_attributes, + 'div' => $common_attributes, + 'ul' => $common_attributes, + 'li' => $common_attributes, + 'ol' => $common_attributes, + 'h1' => $common_attributes, + 'h2' => $common_attributes, + 'h3' => $common_attributes, + 'h4' => $common_attributes, + 'h5' => $common_attributes, + 'h6' => $common_attributes, + 'button' => $common_attributes, + 'sup' => $common_attributes, + 'sub' => $common_attributes, + 'nobr' => $common_attributes, + ); + } + } + + if ( ! function_exists( 'fs_html_get_classname' ) ) { + /** + * Gets an HTML class attribute value. + * + * @param string|string[] $classes + * + * @return string + */ + function fs_html_get_classname( $classes ) { + if ( is_array( $classes ) ) { + $classes = implode( ' ', $classes ); + } + + return esc_attr( $classes ); + } + } + + if ( ! function_exists( 'fs_html_get_attributes' ) ) { + /** + * Gets a properly escaped HTML attributes string from an associative array. + * + * @param array $attributes A key/value pair array of attributes. + * + * @return string + */ + function fs_html_get_attributes( $attributes ) { + $attribute_string = ''; + + foreach ( $attributes as $key => $value ) { + $attribute_string .= sprintf( + ' %1$s="%2$s"', + esc_attr( $key ), + esc_attr( $value ) + ); + } + + return $attribute_string; + } + } + + if ( ! function_exists( 'fs_html_get_sanitized_html' ) ) { + /** + * Get sanitized HTML for template files. + * + * @param string $raw_html + * + * @return string + * @since 2.5.10 + */ + function fs_html_get_sanitized_html( $raw_html ) { + return wp_kses( $raw_html, fs_html_get_allowed_kses_list() ); + } + } diff --git a/freemius/includes/fs-plugin-info-dialog.php b/freemius/includes/fs-plugin-info-dialog.php index bcfce99..3fbd113 100644 --- a/freemius/includes/fs-plugin-info-dialog.php +++ b/freemius/includes/fs-plugin-info-dialog.php @@ -311,6 +311,7 @@ function _get_addon_info_filter( $data, $action = '', $args = null ) { $data->version = $latest->version; $data->last_updated = $latest->created; $data->requires = $latest->requires_platform_version; + $data->requires_php = $latest->requires_programming_language_version; $data->tested = $latest->tested_up_to_version; } else if ( ! empty( $current_addon_version ) ) { $data->version = $current_addon_version; @@ -1306,8 +1307,8 @@ class="fs-annual-discount">
    -
    +
    @@ -1344,7 +1345,10 @@ class="fs-annual-discount">
  • slug ) ?> - : slug ), $api->requires ) ) ?> + : slug ), $api->requires ) + ) ?>
  • requires_php ) ) { + ?> +
  • + slug ); ?>: + slug ), $api->requires_php ) + ); + ?> +
  • + downloaded ) ) { ?>
  • @@ -1485,9 +1502,43 @@ class="fs-annual-discount">
    tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) ) { + $requires_php = isset( $api->requires_php ) ? $api->requires_php : null; + $requires_wp = isset( $api->requires ) ? $api->requires : null; + + $compatible_php = empty( $requires_php ) || version_compare( PHP_VERSION, $requires_php, '>=' ); + + // Strip off any -alpha, -RC, -beta, -src suffixes. + list( $wp_version ) = explode( '-', $GLOBALS['wp_version'] ); + + $compatible_wp = empty( $requires_wp ) || version_compare( $wp_version, $requires_wp, '>=' ); + $tested_wp = ( empty( $api->tested ) || version_compare( $wp_version, $api->tested, '<=' ) ); + + if ( ! $compatible_php ) { + echo '

    ' . fs_text_inline( 'Error', 'error', $api->slug ) . ': ' . fs_text_inline( 'This plugin requires a newer version of PHP.', 'newer-php-required-error', $api->slug ); + + if ( current_user_can( 'update_php' ) ) { + $wp_get_update_php_url = function_exists( 'wp_get_update_php_url' ) ? + wp_get_update_php_url() : + 'https://wordpress.org/support/update-php/'; + + printf( + /* translators: %s: URL to Update PHP page. */ + ' ' . fs_text_inline( 'Click here to learn more about updating PHP.', 'php-update-learn-more-link', $api->slug ), + esc_url( $wp_get_update_php_url ) + ); + + if ( function_exists( 'wp_update_php_annotation' ) ) { + wp_update_php_annotation( '

    ', '' ); + } + } else { + echo '

    '; + } + echo '
    '; + } + + if ( ! $tested_wp ) { echo '

    ' . '' . fs_text_inline( 'Warning', 'warning', $api->slug ) . ': ' . fs_text_inline( 'This plugin has not been tested with your current version of WordPress.', 'not-tested-warning', $api->slug ) . '

    '; - } else if ( ! empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) ) { + } else if ( ! $compatible_wp ) { echo '

    ' . '' . fs_text_inline( 'Warning', 'warning', $api->slug ) . ': ' . fs_text_inline( 'This plugin has not been marked as compatible with your version of WordPress.', 'not-compatible-warning', $api->slug ) . '

    '; } diff --git a/freemius/includes/managers/class-fs-admin-notice-manager.php b/freemius/includes/managers/class-fs-admin-notice-manager.php index b238d9b..3734be1 100644 --- a/freemius/includes/managers/class-fs-admin-notice-manager.php +++ b/freemius/includes/managers/class-fs-admin-notice-manager.php @@ -160,7 +160,10 @@ protected function __construct( false, isset( $msg['wp_user_id'] ) ? $msg['wp_user_id'] : null, ! empty( $msg['plugin'] ) ? $msg['plugin'] : null, - $is_network_and_blog_admins + $is_network_and_blog_admins, + isset( $msg['dismissible'] ) ? + $msg['dismissible'] : + null ); } } @@ -224,9 +227,6 @@ function _admin_notices_hook() { return; } - - $show_admin_notices = ( ! $this->is_gutenberg_page() ); - foreach ( $this->_notices as $id => $msg ) { if ( isset( $msg['wp_user_id'] ) && is_numeric( $msg['wp_user_id'] ) ) { if ( get_current_user_id() != $msg['wp_user_id'] ) { @@ -269,7 +269,7 @@ function _admin_notices_hook() { $show_notice = call_user_func_array( 'fs_apply_filter', array( $this->_module_unique_affix, 'show_admin_notice', - $show_admin_notices, + $this->show_admin_notices(), $msg ) ); @@ -323,6 +323,30 @@ function is_gutenberg_page() { return false; } + /** + * Check if admin notices should be shown on page. E.g., we don't want to show notices in the Visual Editor. + * + * @author Xiaheng Chen (@xhchen) + * @since 2.4.2 + * + * @return bool + */ + function show_admin_notices() { + global $pagenow; + + if ( 'about.php' === $pagenow ) { + // Don't show admin notices on the About page. + return false; + } + + if ( $this->is_gutenberg_page() ) { + // Don't show admin notices in Gutenberg (visual editor). + return false; + } + + return true; + } + /** * Add admin message to admin messages queue, and hook to admin_notices / all_admin_notices if not yet hooked. * @@ -339,6 +363,8 @@ function is_gutenberg_page() { * @param string|null $plugin_title * @param bool $is_network_and_blog_admins Whether or not the message should be shown both on network * and blog admin pages. + * @param bool|null $is_dismissible + * @param array $data * * @uses add_action() */ @@ -351,7 +377,9 @@ function add( $store_if_sticky = true, $wp_user_id = null, $plugin_title = null, - $is_network_and_blog_admins = false + $is_network_and_blog_admins = false, + $is_dismissible = null, + $data = array() ) { $notices_type = $this->get_notices_type(); @@ -371,14 +399,16 @@ function add( } $message_object = array( - 'message' => $message, - 'title' => $title, - 'type' => $type, - 'sticky' => $is_sticky, - 'id' => $id, - 'manager_id' => $this->_id, - 'plugin' => ( ! is_null( $plugin_title ) ? $plugin_title : $this->_title ), - 'wp_user_id' => $wp_user_id, + 'message' => $message, + 'title' => $title, + 'type' => $type, + 'sticky' => $is_sticky, + 'id' => $id, + 'manager_id' => $this->_id, + 'plugin' => ( ! is_null( $plugin_title ) ? $plugin_title : $this->_title ), + 'wp_user_id' => $wp_user_id, + 'dismissible' => $is_dismissible, + 'data' => $data ); if ( $is_sticky && $store_if_sticky ) { @@ -393,15 +423,16 @@ function add( * @since 1.0.7 * * @param string|string[] $ids + * @param bool $store */ - function remove_sticky( $ids ) { + function remove_sticky( $ids, $store = true ) { if ( ! is_array( $ids ) ) { $ids = array( $ids ); } foreach ( $ids as $id ) { // Remove from sticky storage. - $this->_sticky_storage->remove( $id ); + $this->_sticky_storage->remove( $id, $store ); if ( isset( $this->_notices[ $id ] ) ) { unset( $this->_notices[ $id ] ); @@ -437,14 +468,32 @@ function has_sticky( $id ) { * @param string|null $plugin_title * @param bool $is_network_and_blog_admins Whether or not the message should be shown both on network * and blog admin pages. + * @param bool $is_dimissible + * @param array $data */ - function add_sticky( $message, $id, $title = '', $type = 'success', $wp_user_id = null, $plugin_title = null, $is_network_and_blog_admins = false ) { + function add_sticky( $message, $id, $title = '', $type = 'success', $wp_user_id = null, $plugin_title = null, $is_network_and_blog_admins = false, $is_dimissible = true, $data = array() ) { if ( ! empty( $this->_module_unique_affix ) ) { $message = fs_apply_filter( $this->_module_unique_affix, "sticky_message_{$id}", $message ); $title = fs_apply_filter( $this->_module_unique_affix, "sticky_title_{$id}", $title ); } - $this->add( $message, $title, $type, true, $id, true, $wp_user_id, $plugin_title, $is_network_and_blog_admins ); + $this->add( $message, $title, $type, true, $id, true, $wp_user_id, $plugin_title, $is_network_and_blog_admins, $is_dimissible, $data ); + } + + /** + * Retrieves the data of an sticky notice. + * + * @author Leo Fajardo (@leorw) + * @since 2.4.3 + * + * @param string $id Message ID. + * + * @return array|null + */ + function get_sticky( $id ) { + return isset( $this->_sticky_storage->{$id} ) ? + $this->_sticky_storage->{$id} : + null; } /** @@ -452,9 +501,15 @@ function add_sticky( $message, $id, $title = '', $type = 'success', $wp_user_id * * @author Vova Feldman (@svovaf) * @since 1.0.8 + * + * @param bool $is_temporary @since 2.5.1 */ - function clear_all_sticky() { - $this->_sticky_storage->clear_all(); + function clear_all_sticky( $is_temporary = false ) { + if ( $is_temporary ) { + $this->_notices = array(); + } else { + $this->_sticky_storage->clear_all(); + } } #-------------------------------------------------------------------------------- diff --git a/freemius/includes/managers/class-fs-clone-manager.php b/freemius/includes/managers/class-fs-clone-manager.php new file mode 100644 index 0000000..e992ef1 --- /dev/null +++ b/freemius/includes/managers/class-fs-clone-manager.php @@ -0,0 +1,1660 @@ +_storage = FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true ); + $this->_network_storage = FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true, true ); + + $this->maybe_migrate_options(); + + $this->_notices = FS_Admin_Notices::instance( 'global_clone_resolution_notices', '', '', true ); + $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . '_clone_manager', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK ); + } + + /** + * Migrate clone resolution options from 2.5.0 array-based structure, to a new flat structure. + * + * The reason this logic is not in a separate migration script is that we want to be 100% sure data is migrated before any execution of clone logic. + * + * @todo Delete this one in the future. + */ + private function maybe_migrate_options() { + $storages = array( + $this->_storage, + $this->_network_storage + ); + + foreach ( $storages as $storage ) { + $clone_data = $storage->get_option( self::OPTION_NAME ); + if ( is_array( $clone_data ) && ! empty( $clone_data ) ) { + foreach ( $clone_data as $key => $val ) { + if ( ! is_null( $val ) ) { + $storage->set_option( $key, $val ); + } + } + + $storage->unset_option( self::OPTION_NAME, true ); + } + } + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _init() { + if ( is_admin() ) { + if ( Freemius::is_admin_post() ) { + add_action( 'admin_post_fs_clone_resolution', array( $this, '_handle_clone_resolution' ) ); + } + + if ( Freemius::is_ajax() ) { + Freemius::add_ajax_action_static( 'handle_clone_resolution', array( $this, '_clone_resolution_action_ajax_handler' ) ); + } else { + if ( + empty( $this->get_clone_identification_timestamp() ) && + ( + ! fs_is_network_admin() || + ! ( $this->is_clone_resolution_options_notice_shown() || $this->is_temporary_duplicate_notice_shown() ) + ) + ) { + $this->hide_clone_admin_notices(); + } else if ( ! Freemius::is_cron() && ! Freemius::is_admin_post() ) { + $this->try_resolve_clone_automatically(); + $this->maybe_show_clone_admin_notice(); + + add_action( 'admin_footer', array( $this, '_add_clone_resolution_javascript' ) ); + } + } + } + } + + /** + * Retrieves the timestamp that was stored when a clone was identified. + * + * @return int|null + */ + function get_clone_identification_timestamp() { + return $this->get_option( 'clone_identification_timestamp', true ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + * + * @param string $sdk_last_version + */ + function maybe_update_clone_resolution_support_flag( $sdk_last_version ) { + if ( null !== $this->hide_manual_resolution ) { + return; + } + + $this->hide_manual_resolution = ( + ! empty( $sdk_last_version ) && + version_compare( $sdk_last_version, '2.5.0', '<' ) + ); + } + + /** + * Stores the time when a clone was identified. + */ + function store_clone_identification_timestamp() { + $this->clone_identification_timestamp = time(); + } + + /** + * Retrieves the timestamp for the temporary duplicate mode's expiration. + * + * @return int + */ + function get_temporary_duplicate_expiration_timestamp() { + $temporary_duplicate_mode_start_timestamp = $this->was_temporary_duplicate_mode_selected() ? + $this->temporary_duplicate_mode_selection_timestamp : + $this->get_clone_identification_timestamp(); + + return ( $temporary_duplicate_mode_start_timestamp + self::TEMPORARY_DUPLICATE_PERIOD ); + } + + /** + * Determines if the SDK should handle clones. The SDK handles clones only up to 3 times with 3 min interval. + * + * @return bool + */ + private function should_handle_clones() { + if ( ! isset( $this->request_handler_timestamp ) ) { + return true; + } + + if ( $this->request_handler_retries_count >= self::CLONE_RESOLUTION_MAX_RETRIES ) { + return false; + } + + // Give the logic that handles clones enough time to finish (it is given 3 minutes for now). + return ( time() > ( $this->request_handler_timestamp + self::CLONE_RESOLUTION_MAX_EXECUTION_TIME ) ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + * + * @return bool + */ + function should_hide_manual_resolution() { + return ( true === $this->hide_manual_resolution ); + } + + /** + * Executes the clones handler logic if it should be executed, i.e., based on the return value of the should_handle_clones() method. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function maybe_run_clone_resolution() { + if ( ! $this->should_handle_clones() ) { + return; + } + + $this->request_handler_retries_count = isset( $this->request_handler_retries_count ) ? + ( $this->request_handler_retries_count + 1 ) : + 1; + + $this->request_handler_timestamp = time(); + + $handler_id = ( rand() . microtime() ); + $this->request_handler_id = $handler_id; + + // Add cookies to trigger request with the same user access permissions. + $cookies = array(); + foreach ( $_COOKIE as $name => $value ) { + $cookies[] = new WP_Http_Cookie( array( + 'name' => $name, + 'value' => $value, + ) ); + } + + wp_remote_post( + admin_url( 'admin-post.php' ), + array( + 'method' => 'POST', + 'body' => array( + 'action' => 'fs_clone_resolution', + 'handler_id' => $handler_id, + ), + 'timeout' => 0.01, + 'blocking' => false, + 'sslverify' => false, + 'cookies' => $cookies, + ) + ); + } + + /** + * Executes the clones handler logic. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _handle_clone_resolution() { + $handler_id = fs_request_get( 'handler_id' ); + + if ( empty( $handler_id ) ) { + return; + } + + if ( + ! isset( $this->request_handler_id ) || + $this->request_handler_id !== $handler_id + ) { + return; + } + + if ( ! $this->try_automatic_resolution() ) { + $this->clear_temporary_duplicate_notice_shown_timestamp(); + } + } + + #-------------------------------------------------------------------------------- + #region Automatic Clone Resolution + #-------------------------------------------------------------------------------- + + /** + * @var array All installs cache. + */ + private $all_installs; + + /** + * Checks if a given instance's install is a clone of another subsite in the network. + * + * @author Vova Feldman (@svovaf) + * + * @return FS_Site + */ + private function find_network_subsite_clone_install( Freemius $instance ) { + if ( ! is_multisite() ) { + // Not a multi-site network. + return null; + } + + if ( ! isset( $this->all_installs ) ) { + $this->all_installs = Freemius::get_all_modules_sites(); + } + + // Check if there's another blog that has the same site. + $module_type = $instance->get_module_type(); + $sites_by_module_type = ! empty( $this->all_installs[ $module_type ] ) ? + $this->all_installs[ $module_type ] : + array(); + + $slug = $instance->get_slug(); + $sites_by_slug = ! empty( $sites_by_module_type[ $slug ] ) ? + $sites_by_module_type[ $slug ] : + array(); + + $current_blog_id = get_current_blog_id(); + + $current_install = $instance->get_site(); + + foreach ( $sites_by_slug as $site ) { + if ( + $current_install->id == $site->id && + $current_blog_id != $site->blog_id + ) { + // Clone is identical to an install on another subsite in the network. + return $site; + } + } + + return null; + } + + /** + * Tries to find a different install of the context product that is associated with the current URL and loads it. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param Freemius $instance + * @param string $url + * + * @return object + */ + private function find_other_install_by_url( Freemius $instance, $url ) { + $result = $instance->get_api_user_scope()->get( "/plugins/{$instance->get_id()}/installs.json?url=" . urlencode( $url ) . "&all=true", true ); + + $current_install = $instance->get_site(); + + if ( $instance->is_api_result_object( $result, 'installs' ) ) { + foreach ( $result->installs as $install ) { + if ( $install->id == $current_install->id ) { + continue; + } + + if ( + $instance->is_only_premium() && + ! FS_Plugin_License::is_valid_id( $install->license_id ) + ) { + continue; + } + + // When searching for installs by a URL, the API will first strip any paths and search for any matching installs by the subdomain. Therefore, we need to test if there's a match between the current URL and the install's URL before continuing. + if ( $url !== fs_strip_url_protocol( untrailingslashit( $install->url ) ) ) { + continue; + } + + // Found a different install that is associated with the current URL, load it and replace the current install with it if no updated install is found. + return $install; + } + } + + return null; + } + + /** + * Delete the current install associated with a given instance and opt-in/activate-license to create a fresh install. + * + * @author Vova Feldman (@svovaf) + * @since 2.5.0 + * + * @param Freemius $instance + * @param string|false $license_key + * + * @return bool TRUE if successfully connected. FALSE if failed and had to restore install from backup. + */ + private function delete_install_and_connect( Freemius $instance, $license_key = false ) { + $user = Freemius::_get_user_by_id( $instance->get_site()->user_id ); + + $instance->delete_current_install( true ); + + if ( ! is_object( $user ) ) { + // Get logged-in WordPress user. + $current_user = Freemius::_get_current_wp_user(); + + // Find the relevant FS user by email address. + $user = Freemius::_get_user_by_email( $current_user->user_email ); + } + + if ( is_object( $user ) ) { + // When a clone is found, we prefer to use the same user of the original install for the opt-in. + $instance->install_with_user( $user, $license_key, false, false ); + } else { + // If no user is found, activate with the license. + $instance->opt_in( + false, + false, + false, + $license_key + ); + } + + if ( is_object( $instance->get_site() ) ) { + // Install successfully created. + return true; + } + + // Restore from backup. + $instance->restore_backup_site(); + + return false; + } + + /** + * Try to resolve the clone situation automatically. + * + * @param Freemius $instance + * @param string $current_url + * @param bool $is_localhost + * @param bool|null $is_clone_of_network_subsite + * + * @return bool If managed to automatically resolve the clone. + */ + private function try_resolve_clone_automatically_by_instance( + Freemius $instance, + $current_url, + $is_localhost, + $is_clone_of_network_subsite = null + ) { + // Try to find a different install of the context product that is associated with the current URL. + $associated_install = $this->find_other_install_by_url( $instance, $current_url ); + + if ( is_object( $associated_install ) ) { + // Replace the current install with a different install that is associated with the current URL. + $instance->store_site( new FS_Site( clone $associated_install ) ); + $instance->sync_install( array( 'is_new_site' => true ), true ); + + return true; + } + + if ( ! $instance->is_premium() ) { + // For free products, opt-in with the context user to create new install. + return $this->delete_install_and_connect( $instance ); + } + + $license = $instance->_get_license(); + $can_activate_license = ( is_object( $license ) && ! $license->is_utilized( $is_localhost ) ); + + if ( ! $can_activate_license ) { + // License can't be activated, therefore, can't be automatically resolved. + return false; + } + + if ( ! WP_FS__IS_LOCALHOST_FOR_SERVER && ! $is_localhost ) { + $is_clone_of_network_subsite = ( ! is_null( $is_clone_of_network_subsite ) ) ? + $is_clone_of_network_subsite : + is_object( $this->find_network_subsite_clone_install( $instance ) ); + + if ( ! $is_clone_of_network_subsite ) { + return false; + } + } + + // If the site is a clone of another subsite in the network, or a localhost one, try to auto activate the license. + return $this->delete_install_and_connect( $instance, $license->secret_key ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + private function try_resolve_clone_automatically() { + $clone_action = $this->get_clone_resolution_action_from_config(); + + if ( ! empty( $clone_action ) ) { + $this->try_resolve_clone_automatically_by_config( $clone_action ); + return; + } + + $this->try_automatic_resolution(); + } + + /** + * Tries to resolve the clone situation automatically based on the config in the wp-config.php file. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param string $clone_action + */ + private function try_resolve_clone_automatically_by_config( $clone_action ) { + $fs_instances = array(); + + if ( self::OPTION_LONG_TERM_DUPLICATE === $clone_action ) { + $instances = Freemius::_get_all_instances(); + + foreach ( $instances as $instance ) { + if ( ! $instance->is_registered() ) { + continue; + } + + if ( ! $instance->is_clone() ) { + continue; + } + + $license = $instance->has_features_enabled_license() ? + $instance->_get_license() : + null; + + if ( + is_object( $license ) && + ! $license->is_utilized( + ( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( Freemius::get_unfiltered_site_url() ) ) + ) + ) { + $fs_instances[] = $instance; + } + } + + if ( empty( $fs_instances ) ) { + return; + } + } + + $this->resolve_cloned_sites( $clone_action, $fs_instances ); + } + + /** + * @author Leo Fajard (@leorw) + * @since 2.5.0 + * + * @return string|null + */ + private function get_clone_resolution_action_from_config() { + if ( ! defined( 'FS__RESOLVE_CLONE_AS' ) ) { + return null; + } + + if ( ! in_array( + FS__RESOLVE_CLONE_AS, + array( + self::OPTION_NEW_HOME, + self::OPTION_TEMPORARY_DUPLICATE, + self::OPTION_LONG_TERM_DUPLICATE, + ) + ) ) { + return null; + } + + return FS__RESOLVE_CLONE_AS; + } + + /** + * Tries to recover the install of a newly created subsite or resolve it if it's a clone. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param Freemius $instance + */ + function maybe_resolve_new_subsite_install_automatically( Freemius $instance ) { + if ( ! $instance->is_user_in_admin() ) { + // Try to recover an install or resolve a clone only when there's a user in admin to prevent doing it prematurely (e.g., the install can get replaced with clone data again). + return; + } + + if ( ! is_multisite() ) { + return; + } + + $new_blog_install_map = $this->new_blog_install_map; + + if ( empty( $new_blog_install_map ) || ! is_array( $new_blog_install_map ) ) { + return; + } + + $is_network_admin = fs_is_network_admin(); + + if ( ! $is_network_admin ) { + // If not in network admin, handle the current site. + $blog_id = get_current_blog_id(); + } else { + // If in network admin, handle only the first site. + $blog_ids = array_keys( $new_blog_install_map ); + $blog_id = $blog_ids[0]; + } + + if ( ! isset( $new_blog_install_map[ $blog_id ] ) ) { + // There's no site to handle. + return; + } + + $expected_install_id = $new_blog_install_map[ $blog_id ]['install_id']; + + $current_install = $instance->get_install_by_blog_id( $blog_id ); + $current_install_id = is_object( $current_install ) ? + $current_install->id : + null; + + if ( $expected_install_id == $current_install_id ) { + // Remove the current site's information from the map to prevent handling it again. + $this->remove_new_blog_install_info_from_storage( $blog_id ); + + return; + } + + require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php'; + + $lock = new FS_Lock( self::OPTION_NAME . '_subsite' ); + + if ( ! $lock->try_lock(60) ) { + return; + } + + $instance->switch_to_blog( $blog_id ); + + $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) ); + $current_install_url = is_object( $current_install ) ? + fs_strip_url_protocol( untrailingslashit( $current_install->url ) ) : + null; + + // This can be `false` even if the install is a clone as the URL can be updated as part of the cloning process. + $is_clone = ( ! is_null( $current_install_url ) && $current_url !== $current_install_url ); + + if ( ! FS_Site::is_valid_id( $expected_install_id ) ) { + $expected_install = null; + } else { + $expected_install = $instance->fetch_install_by_id( $expected_install_id ); + } + + if ( FS_Api::is_api_result_entity( $expected_install ) ) { + // Replace the current install with the expected install. + $instance->store_site( new FS_Site( clone $expected_install ) ); + $instance->sync_install( array( 'is_new_site' => true ), true ); + } else { + $network_subsite_clone_install = null; + + if ( ! $is_clone ) { + // It is possible that `$is_clone` is `false` but the install is actually a clone as the following call checks the install ID and not the URL. + $network_subsite_clone_install = $this->find_network_subsite_clone_install( $instance ); + } + + if ( $is_clone || is_object( $network_subsite_clone_install ) ) { + // If there's no expected install (or it couldn't be fetched) and the current install is a clone, try to resolve the clone automatically. + $is_localhost = FS_Site::is_localhost_by_address( $current_url ); + + $resolved = $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost, is_object( $network_subsite_clone_install ) ); + + if ( ! $resolved && is_object( $network_subsite_clone_install ) ) { + if ( empty( $this->get_clone_identification_timestamp() ) ) { + $this->store_clone_identification_timestamp(); + } + + // Since the clone couldn't be identified based on the URL, replace the stored install with the cloned install so that the manual clone resolution notice will appear. + $instance->store_site( clone $network_subsite_clone_install ); + } + } + } + + $instance->restore_current_blog(); + + // Remove the current site's information from the map to prevent handling it again. + $this->remove_new_blog_install_info_from_storage( $blog_id ); + + $lock->unlock(); + } + + /** + * If a new install was created after creating a new subsite, its ID is stored in the blog-install map so that it can be recovered in case it's replaced with a clone install (e.g., when the newly created subsite is a clone). The IDs of the clone subsites that were created while not running this version of the SDK or a higher version will also be stored in the said map so that the clone manager can also try to resolve them later on. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param int $blog_id + * @param FS_Site $site + */ + function store_blog_install_info( $blog_id, $site = null ) { + $new_blog_install_map = $this->new_blog_install_map; + + if ( + empty( $new_blog_install_map ) || + ! is_array( $new_blog_install_map ) + ) { + $new_blog_install_map = array(); + } + + $install_id = null; + + if ( is_object( $site ) ) { + $install_id = $site->id; + } + + $new_blog_install_map[ $blog_id ] = array( 'install_id' => $install_id ); + + $this->new_blog_install_map = $new_blog_install_map; + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param int $blog_id + */ + private function remove_new_blog_install_info_from_storage( $blog_id ) { + $new_blog_install_map = $this->new_blog_install_map; + + unset( $new_blog_install_map[ $blog_id ] ); + $this->new_blog_install_map = $new_blog_install_map; + } + + /** + * Tries to resolve all clones automatically. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return bool If managed to automatically resolve all clones. + */ + private function try_automatic_resolution() { + $this->_logger->entrance(); + + require_once WP_FS__DIR_INCLUDES . '/class-fs-lock.php'; + + $lock = new FS_Lock( self::OPTION_NAME ); + + /** + * Try to acquire lock for the next 60 sec based on the thread ID. + */ + if ( ! $lock->try_lock( 60 ) ) { + return false; + } + + $current_url = untrailingslashit( Freemius::get_unfiltered_site_url( null, true ) ); + $is_localhost = FS_Site::is_localhost_by_address( $current_url ); + + $require_manual_resolution = false; + + $instances = Freemius::_get_all_instances(); + + foreach ( $instances as $instance ) { + if ( ! $instance->is_registered() ) { + continue; + } + + if ( ! $instance->is_clone() ) { + continue; + } + + if ( ! $this->try_resolve_clone_automatically_by_instance( $instance, $current_url, $is_localhost ) ) { + $require_manual_resolution = true; + } + } + + // Create a 1-day lock. + $lock->lock( WP_FS__TIME_24_HOURS_IN_SEC ); + + return ( ! $require_manual_resolution ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Manual Clone Resolution + #-------------------------------------------------------------------------------- + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _add_clone_resolution_javascript() { + $vars = array( 'ajax_action' => Freemius::get_ajax_action_static( 'handle_clone_resolution' ) ); + + fs_require_once_template( 'clone-resolution-js.php', $vars ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function _clone_resolution_action_ajax_handler() { + $this->_logger->entrance(); + + check_ajax_referer( Freemius::get_ajax_action_static( 'handle_clone_resolution' ), 'security' ); + + $clone_action = fs_request_get( 'clone_action' ); + $blog_id = is_multisite() ? + fs_request_get( 'blog_id' ) : + 0; + + if ( is_multisite() && $blog_id == get_current_blog_id() ) { + $blog_id = 0; + } + + if ( empty( $clone_action ) ) { + Freemius::shoot_ajax_failure( array( + 'message' => fs_text_inline( 'Invalid clone resolution action.', 'invalid-clone-resolution-action-error' ), + 'redirect_url' => '', + ) ); + } + + $result = $this->resolve_cloned_sites( $clone_action, array(), $blog_id ); + + Freemius::shoot_ajax_success( $result ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @param string $clone_action + * @param Freemius[] $fs_instances + * @param int $blog_id + * + * @return array + */ + private function resolve_cloned_sites( $clone_action, $fs_instances = array(), $blog_id = 0 ) { + $this->_logger->entrance(); + + $result = array(); + + $instances_with_clone = array(); + $instances_with_clone_count = 0; + $install_by_instance_id = array(); + + $instances = ( ! empty( $fs_instances ) ) ? + $fs_instances : + Freemius::_get_all_instances(); + + $should_switch_to_blog = ( $blog_id > 0 ); + + foreach ( $instances as $instance ) { + if ( $should_switch_to_blog ) { + $instance->switch_to_blog( $blog_id ); + } + + if ( $instance->is_registered() && $instance->is_clone() ) { + $instances_with_clone[] = $instance; + + $instances_with_clone_count ++; + + $install_by_instance_id[ $instance->get_id() ] = $instance->get_site(); + } + } + + if ( self::OPTION_TEMPORARY_DUPLICATE === $clone_action ) { + $this->store_temporary_duplicate_timestamp(); + } else { + $redirect_url = ''; + + foreach ( $instances_with_clone as $instance ) { + if ( $should_switch_to_blog ) { + $instance->switch_to_blog( $blog_id ); + } + + $has_error = false; + + if ( self::OPTION_NEW_HOME === $clone_action ) { + $instance->sync_install( array( 'is_new_site' => true ), true ); + + if ( $instance->is_clone() ) { + $has_error = true; + } + } else { + $instance->_handle_long_term_duplicate(); + + if ( ! is_object( $instance->get_site() ) ) { + $has_error = true; + } + } + + if ( $has_error && 1 === $instances_with_clone_count ) { + $redirect_url = $instance->get_activation_url(); + } + } + + $result = ( array( 'redirect_url' => $redirect_url ) ); + } + + foreach ( $instances_with_clone as $instance ) { + if ( $should_switch_to_blog ) { + $instance->switch_to_blog( $blog_id ); + } + + // No longer a clone, send an update. + if ( ! $instance->is_clone() ) { + $instance->send_clone_resolution_update( + $clone_action, + $install_by_instance_id[ $instance->get_id() ] + ); + } + } + + if ( 'temporary_duplicate_license_activation' !== $clone_action ) { + $this->remove_clone_resolution_options_notice(); + } else { + $this->remove_temporary_duplicate_notice(); + } + + if ( $should_switch_to_blog ) { + foreach ( $instances as $instance ) { + $instance->restore_current_blog(); + } + } + + return $result; + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + private function hide_clone_admin_notices() { + $this->remove_clone_resolution_options_notice( false ); + $this->remove_temporary_duplicate_notice( false ); + } + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + function maybe_show_clone_admin_notice() { + $this->_logger->entrance(); + + if ( fs_is_network_admin() ) { + $existing_notice_ids = $this->maybe_remove_notices(); + + if ( ! empty( $existing_notice_ids ) ) { + fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' ); + } + + return; + } + + $first_instance_with_clone = null; + + $site_urls = array(); + $sites_with_license_urls = array(); + $sites_with_premium_version_count = 0; + $product_ids = array(); + $product_titles = array(); + + $instances = Freemius::_get_all_instances(); + + foreach ( $instances as $instance ) { + if ( ! $instance->is_registered() ) { + continue; + } + + if ( ! $instance->is_clone( true ) ) { + continue; + } + + $install = $instance->get_site(); + + $site_urls[] = $install->url; + $product_ids[] = $instance->get_id(); + $product_titles[] = $instance->get_plugin_title(); + + if ( is_null( $first_instance_with_clone ) ) { + $first_instance_with_clone = $instance; + } + + if ( is_object( $instance->_get_license() ) ) { + $sites_with_license_urls[] = $install->url; + } + + if ( $instance->is_premium() ) { + $sites_with_premium_version_count ++; + } + } + + if ( empty( $site_urls ) && empty( $sites_with_license_urls ) ) { + $this->hide_clone_admin_notices(); + + return; + } + + $site_urls = array_unique( $site_urls ); + $sites_with_license_urls = array_unique( $sites_with_license_urls ); + + $module_label = fs_text_inline( 'products', 'products' ); + $admin_notice_module_title = null; + + $has_temporary_duplicate_mode_expired = $this->has_temporary_duplicate_mode_expired(); + + if ( + ! $this->was_temporary_duplicate_mode_selected() || + $has_temporary_duplicate_mode_expired + ) { + if ( ! empty( $site_urls ) ) { + fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' ); + + $doc_url = 'https://freemius.com/help/documentation/wordpress-sdk/safe-mode-clone-resolution-duplicate-website/'; + + if ( 1 === count( $instances ) ) { + $doc_url = fs_apply_filter( + $first_instance_with_clone->get_unique_affix(), + 'clone_resolution_documentation_url', + $doc_url + ); + } + + $this->add_manual_clone_resolution_admin_notice( + $product_ids, + $product_titles, + $site_urls, + Freemius::get_unfiltered_site_url(), + ( count( $site_urls ) === count( $sites_with_license_urls ) ), + ( count( $site_urls ) === $sites_with_premium_version_count ), + $doc_url + ); + } + + return; + } + + if ( empty( $sites_with_license_urls ) ) { + return; + } + + if ( ! $this->is_temporary_duplicate_notice_shown() ) { + $last_time_temporary_duplicate_notice_shown = $this->temporary_duplicate_notice_shown_timestamp; + $was_temporary_duplicate_notice_shown_before = is_numeric( $last_time_temporary_duplicate_notice_shown ); + + if ( $was_temporary_duplicate_notice_shown_before ) { + $temporary_duplicate_mode_expiration_timestamp = $this->get_temporary_duplicate_expiration_timestamp(); + $current_time = time(); + + if ( + $current_time > $temporary_duplicate_mode_expiration_timestamp || + $current_time < ( $temporary_duplicate_mode_expiration_timestamp - ( 2 * WP_FS__TIME_24_HOURS_IN_SEC ) ) + ) { + // Do not show the notice if the temporary duplicate mode has already expired or it will expire more than 2 days from now. + return; + } + } + } + + if ( 1 === count( $sites_with_license_urls ) ) { + $module_label = $first_instance_with_clone->get_module_label( true ); + $admin_notice_module_title = $first_instance_with_clone->get_plugin_title(); + } + + fs_enqueue_local_style( 'fs_clone_resolution_notice', '/admin/clone-resolution.css' ); + + $this->add_temporary_duplicate_sticky_notice( + $product_ids, + $this->get_temporary_duplicate_admin_notice_string( $sites_with_license_urls, $product_titles, $module_label ), + $admin_notice_module_title + ); + } + + /** + * Removes the notices from the storage if the context product is either no longer active on the context subsite or it's active but there's no longer any clone. This prevents the notices from being shown on the network-level admin page when they are no longer relevant. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.1 + * + * @return string[] + */ + private function maybe_remove_notices() { + $notices = array( + 'clone_resolution_options_notice' => $this->_notices->get_sticky( 'clone_resolution_options_notice', true ), + 'temporary_duplicate_notice' => $this->_notices->get_sticky( 'temporary_duplicate_notice', true ), + ); + + $instances = Freemius::_get_all_instances(); + + foreach ( $notices as $id => $notice ) { + if ( ! is_array( $notice ) ) { + unset( $notices[ $id ] ); + continue; + } + + if ( empty( $notice['data'] ) || ! is_array( $notice['data'] ) ) { + continue; + } + + if ( empty( $notice['data']['product_ids'] ) || empty( $notice['data']['blog_id'] ) ) { + continue; + } + + $product_ids = $notice['data']['product_ids']; + $blog_id = $notice['data']['blog_id']; + $has_clone = false; + + if ( ! is_null( get_site( $blog_id ) ) ) { + foreach ( $product_ids as $product_id ) { + if ( ! isset( $instances[ 'm_' . $product_id ] ) ) { + continue; + } + + $instance = $instances[ 'm_' . $product_id ]; + + $plugin_basename = $instance->get_plugin_basename(); + + $is_plugin_active = is_plugin_active_for_network( $plugin_basename ); + + if ( ! $is_plugin_active ) { + switch_to_blog( $blog_id ); + + $is_plugin_active = is_plugin_active( $plugin_basename ); + + restore_current_blog(); + } + + if ( ! $is_plugin_active ) { + continue; + } + + $install = $instance->get_install_by_blog_id( $blog_id ); + + if ( ! is_object( $install ) ) { + continue; + } + + $subsite_url = Freemius::get_unfiltered_site_url( $blog_id, true, true ); + + $has_clone = ( fs_strip_url_protocol( trailingslashit( $install->url ) ) !== $subsite_url ); + } + } + + if ( ! $has_clone ) { + $this->_notices->remove_sticky( $id, true, false ); + unset( $notices[ $id ] ); + } + } + + return array_keys( $notices ); + } + + /** + * Adds a notice that provides the logged-in WordPress user with manual clone resolution options. + * + * @param number[] $product_ids + * @param string[] $site_urls + * @param string $current_url + * @param bool $has_license + * @param bool $is_premium + * @param string $doc_url + */ + private function add_manual_clone_resolution_admin_notice( + $product_ids, + $product_titles, + $site_urls, + $current_url, + $has_license, + $is_premium, + $doc_url + ) { + $this->_logger->entrance(); + + $total_sites = count( $site_urls ); + $sites_list = ''; + + $total_products = count( $product_titles ); + $products_list = ''; + + if ( 1 === $total_products ) { + $notice_header = sprintf( + '

    %s

    ', + fs_esc_html_inline( '%1$s has been placed into safe mode because we noticed that %2$s is an exact copy of %3$s.', 'single-cloned-site-safe-mode-message' ) + ); + } else { + $notice_header = sprintf( + '

    %s

    ', + ( 1 === $total_sites ) ? + fs_esc_html_inline( 'The products below have been placed into safe mode because we noticed that %2$s is an exact copy of %3$s:%1$s', 'multiple-products-cloned-site-safe-mode-message' ) : + fs_esc_html_inline( 'The products below have been placed into safe mode because we noticed that %2$s is an exact copy of these sites:%3$s%1$s', 'multiple-products-multiple-cloned-sites-safe-mode-message' ) + ); + + foreach ( $product_titles as $product_title ) { + $products_list .= sprintf( '
  • %s
  • ', $product_title ); + } + + $products_list = '
      ' . $products_list . '
    '; + + foreach ( $site_urls as $site_url ) { + $sites_list .= sprintf( + '
  • %s
  • ', + $site_url, + fs_strip_url_protocol( $site_url ) + ); + } + + $sites_list = '
      ' . $sites_list . '
    '; + } + + $remote_site_link = '' . (1 === $total_sites ? + sprintf( + '%s', + $site_urls[0], + fs_strip_url_protocol( $site_urls[0] ) + ) : + fs_text_inline( 'the above-mentioned sites', 'above-mentioned-sites' )) . ''; + + $current_site_link = sprintf( + '%s', + $current_url, + fs_strip_url_protocol( $current_url ) + ); + + $button_template = ''; + $option_template = '
    %s

    %s

    %s
    '; + + $duplicate_option = sprintf( + $option_template, + fs_esc_html_inline( 'Is %2$s a duplicate of %4$s?', 'duplicate-site-confirmation-message' ), + fs_esc_html_inline( 'Yes, %2$s is a duplicate of %4$s for the purpose of testing, staging, or development.', 'duplicate-site-message' ), + ( $this->has_temporary_duplicate_mode_expired() ? + sprintf( + $button_template, + 'long_term_duplicate', + fs_text_inline( 'Long-Term Duplicate', 'long-term-duplicate' ) + ) : + sprintf( + $button_template, + 'temporary_duplicate', + fs_text_inline( 'Duplicate Website', 'duplicate-site' ) + ) ) + ); + + $migration_option = sprintf( + $option_template, + fs_esc_html_inline( 'Is %2$s the new home of %4$s?', 'migrate-site-confirmation-message' ), + sprintf( + fs_esc_html_inline( 'Yes, %%2$s is replacing %%4$s. I would like to migrate my %s from %%4$s to %%2$s.', 'migrate-site-message' ), + ( $has_license ? fs_text_inline( 'license', 'license' ) : fs_text_inline( 'data', 'data' ) ) + ), + sprintf( + $button_template, + 'new_home', + $has_license ? + fs_text_inline( 'Migrate License', 'migrate-product-license' ) : + fs_text_inline( 'Migrate', 'migrate-product-data' ) + ) + ); + + $new_website = sprintf( + $option_template, + fs_esc_html_inline( 'Is %2$s a new website?', 'new-site-confirmation-message' ), + fs_esc_html_inline( 'Yes, %2$s is a new and different website that is separate from %4$s.', 'new-site-message' ) . + ($is_premium ? + ' ' . fs_text_inline( 'It requires license activation.', 'new-site-requires-license-activation-message' ) : + '' + ), + sprintf( + $button_template, + 'new_website', + ( ! $is_premium || ! $has_license ) ? + fs_text_inline( 'New Website', 'new-website' ) : + fs_text_inline( 'Activate License', 'activate-license' ) + ) + ); + + $blog_id = get_current_blog_id(); + + /** + * %1$s - single product's title or product titles list. + * %2$s - site's URL. + * %3$s - single install's URL or install URLs list. + * %4$s - Clone site's link or "the above-mentioned sites" if there are multiple clone sites. + */ + $message = sprintf( + $notice_header . + '
    ' . + $duplicate_option . + $migration_option . + $new_website . '
    ' . + sprintf( '
    Unsure what to do? Read more here.
    ', $doc_url ), + // %1$s + ( 1 === $total_products ? + sprintf( '%s', $product_titles[0] ) : + ( 1 === $total_sites ? + sprintf( '
    %s
    ', $products_list ) : + sprintf( '

    %s:

    %s
    ', fs_esc_html_x_inline( 'Products', 'Clone resolution admin notice products list label', 'products' ), $products_list ) ) + ), + // %2$s + $current_site_link, + // %3$s + ( 1 === $total_sites ? + $remote_site_link : + $sites_list ), + // %4$s + $remote_site_link + ); + + $this->_notices->add_sticky( + $message, + 'clone_resolution_options_notice', + '', + 'warn', + true, + null, + null, + true, + // Intentionally not dismissible. + false, + array( + 'product_ids' => $product_ids, + 'blog_id' => $blog_id + ) + ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Temporary Duplicate (Short Term) + #-------------------------------------------------------------------------------- + + /** + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + * + * @return string + */ + private function get_temporary_duplicate_admin_notice_string( + $site_urls, + $product_titles, + $module_label + ) { + $this->_logger->entrance(); + + $temporary_duplicate_end_date = $this->get_temporary_duplicate_expiration_timestamp(); + $temporary_duplicate_end_date = date( 'M j, Y', $temporary_duplicate_end_date ); + + $current_url = Freemius::get_unfiltered_site_url(); + $current_site_link = sprintf( + '%s', + $current_url, + fs_strip_url_protocol( $current_url ) + ); + + $total_sites = count( $site_urls ); + $sites_list = ''; + + $total_products = count( $product_titles ); + $products_list = ''; + + if ( $total_sites > 1 ) { + foreach ( $site_urls as $site_url ) { + $sites_list .= sprintf( + '
  • %s
  • ', + $site_url, + fs_strip_url_protocol( $site_url ) + ); + } + + $sites_list = '
      ' . $sites_list . '
    '; + } + + if ( $total_products > 1 ) { + foreach ( $product_titles as $product_title ) { + $products_list .= sprintf( '
  • %s
  • ', $product_title ); + } + + $products_list = '
      ' . $products_list . '
    '; + } + + return sprintf( + sprintf( + '
    %s
    ', + ( 1 === $total_sites ? + sprintf( '

    %s

    ', fs_esc_html_inline( 'You marked this website, %s, as a temporary duplicate of %s.', 'temporary-duplicate-message' ) ) : + sprintf( '

    %s:

    ', fs_esc_html_inline( 'You marked this website, %s, as a temporary duplicate of these sites', 'temporary-duplicate-of-sites-message' ) ) . '%s' ) + ) . '%s', + $current_site_link, + ( 1 === $total_sites ? + sprintf( + '%s', + $site_urls[0], + fs_strip_url_protocol( $site_urls[0] ) + ) : + $sites_list ), + sprintf( + '

    %s

    %s

    %s

    ', + esc_attr( admin_url( 'admin-ajax.php?_fs_network_admin=false', 'relative' ) ), + sprintf( + fs_esc_html_inline( "%s automatic security & feature updates and paid functionality will keep working without interruptions until %s (or when your license expires, whatever comes first).", 'duplicate-site-confirmation-message' ), + ( 1 === $total_products ? + sprintf( + fs_esc_html_x_inline( "The %s's", '"The ", e.g.: "The plugin"', 'the-product-x'), + "{$module_label}" + ) : + fs_esc_html_inline( "The following products'", 'the-following-products' ) ), + sprintf( '%s', $temporary_duplicate_end_date ) + ), + ( 1 === $total_products ? + '' : + sprintf( '
    %s
    ', $products_list ) + ), + sprintf( + fs_esc_html_inline( 'If this is a long term duplicate, to keep automatic updates and paid functionality after %s, please %s.', 'duplicate-site-message' ), + sprintf( '%s', $temporary_duplicate_end_date), + sprintf( '%s', fs_esc_html_inline( 'activate a license here', 'activate-license-here' ) ) + ) + ) + ); + } + + /** + * Determines if the temporary duplicate mode has already expired. + * + * @return bool + */ + function has_temporary_duplicate_mode_expired() { + $temporary_duplicate_mode_start_timestamp = $this->was_temporary_duplicate_mode_selected() ? + $this->get_option( 'temporary_duplicate_mode_selection_timestamp', true ) : + $this->get_clone_identification_timestamp(); + + if ( ! is_numeric( $temporary_duplicate_mode_start_timestamp ) ) { + return false; + } + + return ( time() > ( $temporary_duplicate_mode_start_timestamp + self::TEMPORARY_DUPLICATE_PERIOD ) ); + } + + /** + * Determines if the logged-in WordPress user manually selected the temporary duplicate mode for the site. + * + * @return bool + */ + function was_temporary_duplicate_mode_selected() { + return is_numeric( $this->temporary_duplicate_mode_selection_timestamp ); + } + + /** + * Stores the time when the logged-in WordPress user selected the temporary duplicate mode for the site. + */ + private function store_temporary_duplicate_timestamp() { + $this->temporary_duplicate_mode_selection_timestamp = time(); + } + + /** + * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site. + * + * @param bool $store + */ + function remove_clone_resolution_options_notice( $store = true ) { + $this->_notices->remove_sticky( 'clone_resolution_options_notice', true, $store ); + } + + /** + * Removes the notice that is shown when the logged-in WordPress user has selected the temporary duplicate mode for the site. + * + * @param bool $store + */ + function remove_temporary_duplicate_notice( $store = true ) { + $this->_notices->remove_sticky( 'temporary_duplicate_notice', true, $store ); + } + + /** + * Determines if the manual clone resolution options notice is currently being shown. + * + * @return bool + */ + function is_clone_resolution_options_notice_shown() { + return $this->_notices->has_sticky( 'clone_resolution_options_notice', true ); + } + + /** + * Determines if the temporary duplicate notice is currently being shown. + * + * @return bool + */ + function is_temporary_duplicate_notice_shown() { + return $this->_notices->has_sticky( 'temporary_duplicate_notice', true ); + } + + /** + * Determines if a site was marked as a temporary duplicate and if it's still a temporary duplicate. + * + * @return bool + */ + function is_temporary_duplicate_by_blog_id( $blog_id ) { + $timestamp = $this->get_option( 'temporary_duplicate_mode_selection_timestamp', false, $blog_id ); + + return ( + is_numeric( $timestamp ) && + time() < ( $timestamp + self::TEMPORARY_DUPLICATE_PERIOD ) + ); + } + + /** + * Determines the last time the temporary duplicate notice was shown. + * + * @return int|null + */ + function last_time_temporary_duplicate_notice_was_shown() { + return $this->temporary_duplicate_notice_shown_timestamp; + } + + /** + * Clears the time that has been stored when the temporary duplicate notice was shown. + */ + function clear_temporary_duplicate_notice_shown_timestamp() { + unset( $this->temporary_duplicate_notice_shown_timestamp ); + } + + /** + * Adds a temporary duplicate notice that provides the logged-in WordPress user with an option to activate a license for the site. + * + * @param number[] $product_ids + * @param string $message + * @param string|null $plugin_title + */ + function add_temporary_duplicate_sticky_notice( + $product_ids, + $message, + $plugin_title = null + ) { + $this->_logger->entrance(); + + $this->_notices->add_sticky( + $message, + 'temporary_duplicate_notice', + '', + 'promotion', + true, + null, + $plugin_title, + true, + true, + array( + 'product_ids' => $product_ids, + 'blog_id' => get_current_blog_id() + ) + ); + + $this->temporary_duplicate_notice_shown_timestamp = time(); + } + + #endregion + + /** + * @author Leo Fajardo + * @since 2.5.0 + * + * @param string $key + * + * @return bool + */ + private function should_use_network_storage( $key ) { + return ( 'new_blog_install_map' === $key ); + } + + /** + * @param string $key + * @param number|null $blog_id + * + * @return FS_Option_Manager + */ + private function get_storage( $key, $blog_id = null ) { + if ( is_numeric( $blog_id ) ){ + return FS_Option_Manager::get_manager( WP_FS___OPTION_PREFIX . self::OPTION_MANAGER_NAME, true, $blog_id ); + } + + return $this->should_use_network_storage( $key ) ? + $this->_network_storage : + $this->_storage; + } + + /** + * @param string $name + * @param bool $flush + * @param number|null $blog_id + * + * @return mixed + */ + private function get_option( $name, $flush = false, $blog_id = null ) { + return $this->get_storage( $name, $blog_id )->get_option( $name, null, $flush ); + } + + #-------------------------------------------------------------------------------- + #region Magic methods + #-------------------------------------------------------------------------------- + + /** + * @param string $name + * @param int|string $value + */ + function __set( $name, $value ) { + $this->get_storage( $name )->set_option( $name, $value, true ); + } + + /** + * @param string $name + * + * @return bool + */ + function __isset( $name ) { + return $this->get_storage( $name )->has_option( $name, true ); + } + + /** + * @param string $name + */ + function __unset( $name ) { + $this->get_storage( $name )->unset_option( $name, true ); + } + + /** + * @param string $name + * + * @return null|int|string + */ + function __get( $name ) { + return $this->get_option( + $name, + // Reload storage from DB when accessing request_handler_* options to avoid race conditions. + fs_starts_with( $name, 'request_handler' ) + ); + } + + #endregion + } diff --git a/freemius/includes/managers/class-fs-gdpr-manager.php b/freemius/includes/managers/class-fs-gdpr-manager.php index a64abb0..5b89d86 100644 --- a/freemius/includes/managers/class-fs-gdpr-manager.php +++ b/freemius/includes/managers/class-fs-gdpr-manager.php @@ -85,18 +85,6 @@ private function update_option( $name, $value ) { $this->_storage->set_option( $this->_option_name, $this->_data, true ); } - /** - * @author Leo Fajardo (@leorw) - * @since 2.1.0 - * - * @return bool|null - */ - public function is_required() { - return isset( $this->_data['required'] ) ? - $this->_data['required'] : - null; - } - /** * @author Leo Fajardo (@leorw) * @since 2.1.0 diff --git a/freemius/includes/managers/class-fs-option-manager.php b/freemius/includes/managers/class-fs-option-manager.php index 9d59abf..1573847 100644 --- a/freemius/includes/managers/class-fs-option-manager.php +++ b/freemius/includes/managers/class-fs-option-manager.php @@ -11,14 +11,11 @@ } /** - * 3-layer lazy options manager. - * layer 3: Memory - * layer 2: Cache (if there's any caching plugin and if WP_FS__DEBUG_SDK is FALSE) - * layer 1: Database (options table). All options stored as one option record in the DB to reduce number of DB - * queries. + * 2-layer lazy options manager. + * layer 2: Memory + * layer 1: Database (options table). All options stored as one option record in the DB to reduce number of DB queries. * - * If load() is not explicitly called, starts as empty manager. Same thing about saving the data - you have to - * explicitly call store(). + * If load() is not explicitly called, starts as empty manager. Same thing about saving the data - you have to explicitly call store(). * * Class Freemius_Option_Manager */ @@ -157,59 +154,33 @@ static function get_manager( function load( $flush = false ) { $this->_logger->entrance(); - $option_name = $this->get_option_manager_name(); - - if ( $flush || ! isset( $this->_options ) ) { - if ( isset( $this->_options ) ) { - // Clear prev options. - $this->clear(); - } - - $cache_group = $this->get_cache_group(); - - if ( WP_FS__DEBUG_SDK ) { - - // Don't use cache layer in DEBUG mode. - $load_options = empty( $this->_options ); - - } else { - - $this->_options = wp_cache_get( - $option_name, - $cache_group - ); + if ( ! $flush && isset( $this->_options ) ) { + return; + } - $load_options = ( false === $this->_options ); - } + if ( isset( $this->_options ) ) { + // Clear prev options. + $this->clear(); + } - $cached = true; + $option_name = $this->get_option_manager_name(); - if ( $load_options ) { - if ( $this->_is_network_storage ) { - $this->_options = get_site_option( $option_name ); - } else if ( $this->_blog_id > 0 ) { - $this->_options = get_blog_option( $this->_blog_id, $option_name ); - } else { - $this->_options = get_option( $option_name ); - } + if ( $this->_is_network_storage ) { + $this->_options = get_site_option( $option_name ); + } else if ( $this->_blog_id > 0 ) { + $this->_options = get_blog_option( $this->_blog_id, $option_name ); + } else { + $this->_options = get_option( $option_name ); + } - if ( is_string( $this->_options ) ) { - $this->_options = json_decode( $this->_options ); - } + if ( is_string( $this->_options ) ) { + $this->_options = json_decode( $this->_options ); + } // $this->_logger->info('get_option = ' . var_export($this->_options, true)); - if ( false === $this->_options ) { - $this->clear(); - } - - $cached = false; - } - - if ( ! WP_FS__DEBUG_SDK && ! $cached ) { - // Set non encoded cache. - wp_cache_set( $option_name, $this->_options, $cache_group ); - } + if ( false === $this->_options ) { + $this->clear(); } } @@ -272,10 +243,15 @@ function delete() { * @since 1.0.6 * * @param string $option + * @param bool $flush * * @return bool */ - function has_option( $option ) { + function has_option( $option, $flush = false ) { + if ( ! $this->is_loaded() || $flush ) { + $this->load( $flush ); + } + return array_key_exists( $option, $this->_options ); } @@ -285,14 +261,15 @@ function has_option( $option ) { * * @param string $option * @param mixed $default + * @param bool $flush * * @return mixed */ - function get_option( $option, $default = null ) { + function get_option( $option, $default = null, $flush = false ) { $this->_logger->entrance( 'option = ' . $option ); - if ( ! $this->is_loaded() ) { - $this->load(); + if ( ! $this->is_loaded() || $flush ) { + $this->load( $flush ); } if ( is_array( $this->_options ) ) { @@ -310,7 +287,7 @@ function get_option( $option, $default = null ) { /** * If it's an object, return a clone of the object, otherwise, * external changes of the object will actually change the value - * of the object in the options manager which may lead to an unexpected + * of the object in the option manager which may lead to an unexpected * behaviour and data integrity when a store() call is triggered. * * Example: @@ -436,10 +413,6 @@ function store() { } else { update_option( $option_name, $this->_options, $this->_autoload ); } - - if ( ! WP_FS__DEBUG_SDK ) { - wp_cache_set( $option_name, $this->_options, $this->get_cache_group() ); - } } /** @@ -499,23 +472,5 @@ private function get_option_manager_name() { return $this->_id; } - /** - * @author Vova Feldman (@svovaf) - * @since 2.0.0 - * - * @return string - */ - private function get_cache_group() { - $group = WP_FS__SLUG; - - if ( $this->_is_network_storage ) { - $group .= '_ms'; - } else if ( $this->_blog_id > 0 ) { - $group .= "_s{$this->_blog_id}"; - } - - return $group; - } - #endregion } diff --git a/freemius/includes/managers/class-fs-permission-manager.php b/freemius/includes/managers/class-fs-permission-manager.php new file mode 100644 index 0000000..7d75e5d --- /dev/null +++ b/freemius/includes/managers/class-fs-permission-manager.php @@ -0,0 +1,707 @@ + + */ + private static $_instances = array(); + + const PERMISSION_USER = 'user'; + const PERMISSION_SITE = 'site'; + const PERMISSION_EVENTS = 'events'; + const PERMISSION_ESSENTIALS = 'essentials'; + const PERMISSION_DIAGNOSTIC = 'diagnostic'; + const PERMISSION_EXTENSIONS = 'extensions'; + const PERMISSION_NEWSLETTER = 'newsletter'; + + /** + * @param Freemius $fs + * + * @return self + */ + static function instance( Freemius $fs ) { + $id = $fs->get_id(); + + if ( ! isset( self::$_instances[ $id ] ) ) { + self::$_instances[ $id ] = new self( $fs ); + } + + return self::$_instances[ $id ]; + } + + /** + * @param Freemius $fs + */ + protected function __construct( Freemius $fs ) { + $this->_fs = $fs; + $this->_storage = FS_Storage::instance( $fs->get_module_type(), $fs->get_slug() ); + } + + /** + * @return string[] + */ + static function get_all_permission_ids() { + return array( + self::PERMISSION_USER, + self::PERMISSION_SITE, + self::PERMISSION_EVENTS, + self::PERMISSION_ESSENTIALS, + self::PERMISSION_DIAGNOSTIC, + self::PERMISSION_EXTENSIONS, + self::PERMISSION_NEWSLETTER, + ); + } + + /** + * @return string[] + */ + static function get_api_managed_permission_ids() { + return array( + self::PERMISSION_USER, + self::PERMISSION_SITE, + self::PERMISSION_EXTENSIONS, + ); + } + + /** + * @param string $permission + * + * @return bool + */ + static function is_supported_permission( $permission ) { + return in_array( $permission, self::get_all_permission_ids() ); + } + + /** + * @since 2.5.3 + * + * @return bool + */ + function is_premium_context() { + return ( $this->_fs->is_premium() || $this->_fs->can_use_premium_code() ); + } + + /** + * @param bool $is_license_activation + * @param array[] $extra_permissions + * + * @return array[] + */ + function get_permissions( $is_license_activation, array $extra_permissions = array() ) { + return $is_license_activation ? + $this->get_license_activation_permissions( $extra_permissions ) : + $this->get_opt_in_permissions( $extra_permissions ); + } + + #-------------------------------------------------------------------------------- + #region Opt-In Permissions + #-------------------------------------------------------------------------------- + + /** + * @param array[] $extra_permissions + * + * @return array[] + */ + function get_opt_in_permissions( + array $extra_permissions = array(), + $load_default_from_storage = false, + $is_optional = false + ) { + $permissions = array_merge( + $this->get_opt_in_required_permissions( $load_default_from_storage ), + $this->get_opt_in_optional_permissions( $load_default_from_storage, $is_optional ), + $extra_permissions + ); + + return $this->get_sorted_permissions_by_priority( $permissions ); + } + + /** + * @param bool $load_default_from_storage + * + * @return array[] + */ + function get_opt_in_required_permissions( $load_default_from_storage = false ) { + return array( $this->get_user_permission( $load_default_from_storage ) ); + } + + /** + * @param bool $load_default_from_storage + * @param bool $is_optional + * + * @return array[] + */ + function get_opt_in_optional_permissions( + $load_default_from_storage = false, + $is_optional = false + ) { + return array_merge( + $this->get_opt_in_diagnostic_permissions( $load_default_from_storage, $is_optional ), + array( $this->get_extensions_permission( + false, + false, + $load_default_from_storage + ) ) + ); + } + + /** + * @param bool $load_default_from_storage + * @param bool $is_optional + * + * @return array[] + */ + function get_opt_in_diagnostic_permissions( + $load_default_from_storage = false, + $is_optional = false + ) { + // Alias. + $fs = $this->_fs; + + $permissions = array(); + + $permissions[] = $this->get_permission( + self::PERMISSION_SITE, + 'admin-links', + $fs->get_text_inline( 'View Basic Website Info', 'permissions-site' ), + $fs->get_text_inline( 'Homepage URL & title, WP & PHP versions, and site language', 'permissions-site_desc' ), + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + $fs->get_text_inline( 'To provide additional functionality that\'s relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the %s should be translated and tailored to.', 'permissions-site_tooltip' ), + $fs->get_module_label( true ) + ), + 10, + $is_optional, + true, + $load_default_from_storage + ); + + $permissions[] = $this->get_permission( + self::PERMISSION_EVENTS, + 'admin-' . ( $fs->is_plugin() ? 'plugins' : 'appearance' ), + sprintf( $fs->get_text_inline( 'View Basic %s Info', 'permissions-events' ), $fs->get_module_label() ), + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + $fs->get_text_inline( 'Current %s & SDK versions, and if active or uninstalled', 'permissions-events_desc' ), + $fs->get_module_label( true ) + ), + '', + 20, + $is_optional, + true, + $load_default_from_storage + ); + + return $permissions; + } + + #endregion + + #-------------------------------------------------------------------------------- + #region License Activation Permissions + #-------------------------------------------------------------------------------- + + /** + * @param array[] $extra_permissions + * + * @return array[] + */ + function get_license_activation_permissions( + array $extra_permissions = array(), + $include_optional_label = true + ) { + $permissions = array_merge( + $this->get_license_required_permissions(), + $this->get_license_optional_permissions( $include_optional_label ), + $extra_permissions + ); + + return $this->get_sorted_permissions_by_priority( $permissions ); + } + + /** + * @param bool $load_default_from_storage + * + * @return array[] + */ + function get_license_required_permissions( $load_default_from_storage = false ) { + // Alias. + $fs = $this->_fs; + + $permissions = array(); + + $permissions[] = $this->get_permission( + self::PERMISSION_ESSENTIALS, + 'admin-links', + $fs->get_text_inline( 'View License Essentials', 'permissions-essentials' ), + $fs->get_text_inline( + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + 'Homepage URL, %s version, SDK version', + $fs->get_module_label() + ), + 'permissions-essentials_desc' + ), + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + $fs->get_text_inline( 'To let you manage & control where the license is activated and ensure %s security & feature updates are only delivered to websites you authorize.', 'permissions-essentials_tooltip' ), + $fs->get_module_label( true ) + ), + 10, + false, + true, + $load_default_from_storage + ); + + $permissions[] = $this->get_permission( + self::PERMISSION_EVENTS, + 'admin-' . ( $fs->is_plugin() ? 'plugins' : 'appearance' ), + sprintf( $fs->get_text_inline( 'View %s State', 'permissions-events' ), $fs->get_module_label() ), + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + $fs->get_text_inline( 'Is active, deactivated, or uninstalled', 'permissions-events_desc-paid' ), + $fs->get_module_label( true ) + ), + sprintf( $fs->get_text_inline( 'So you can reuse the license when the %s is no longer active.', 'permissions-events_tooltip' ), $fs->get_module_label( true ) ), + 20, + false, + true, + $load_default_from_storage + ); + + return $permissions; + } + + /** + * @return array[] + */ + function get_license_optional_permissions( + $include_optional_label = false, + $load_default_from_storage = false + ) { + return array( + $this->get_diagnostic_permission( $include_optional_label, $load_default_from_storage ), + $this->get_extensions_permission( true, $include_optional_label, $load_default_from_storage ), + ); + } + + /** + * @param bool $include_optional_label + * @param bool $load_default_from_storage + * + * @return array + */ + function get_diagnostic_permission( + $include_optional_label = false, + $load_default_from_storage = false + ) { + return $this->get_permission( + self::PERMISSION_DIAGNOSTIC, + 'wordpress-alt', + $this->_fs->get_text_inline( 'View Diagnostic Info', 'permissions-diagnostic' ) . ( $include_optional_label ? ' (' . $this->_fs->get_text_inline( 'optional' ) . ')' : '' ), + $this->_fs->get_text_inline( 'WordPress & PHP versions, site language & title', 'permissions-diagnostic_desc' ), + sprintf( + /* translators: %s: 'Plugin' or 'Theme' */ + $this->_fs->get_text_inline( 'To avoid breaking your website due to WordPress or PHP version incompatibilities, and recognize which languages & regions the %s should be translated and tailored to.', 'permissions-diagnostic_tooltip' ), + $this->_fs->get_module_label( true ) + ), + 25, + true, + true, + $load_default_from_storage + ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Common Permissions + #-------------------------------------------------------------------------------- + + /** + * @param bool $is_license_activation + * @param bool $include_optional_label + * @param bool $load_default_from_storage + * + * @return array + */ + function get_extensions_permission( + $is_license_activation, + $include_optional_label = false, + $load_default_from_storage = false + ) { + $is_on_by_default = ! $is_license_activation; + + return $this->get_permission( + self::PERMISSION_EXTENSIONS, + 'block-default', + $this->_fs->get_text_inline( 'View Plugins & Themes List', 'permissions-extensions' ) . ( $is_license_activation ? ( $include_optional_label ? ' (' . $this->_fs->get_text_inline( 'optional' ) . ')' : '' ) : '' ), + $this->_fs->get_text_inline( 'Names, slugs, versions, and if active or not', 'permissions-extensions_desc' ), + $this->_fs->get_text_inline( 'To ensure compatibility and avoid conflicts with your installed plugins and themes.', 'permissions-events_tooltip' ), + 25, + true, + $is_on_by_default, + $load_default_from_storage + ); + } + + /** + * @param bool $load_default_from_storage + * + * @return array + */ + function get_user_permission( $load_default_from_storage = false ) { + return $this->get_permission( + self::PERMISSION_USER, + 'admin-users', + $this->_fs->get_text_inline( 'View Basic Profile Info', 'permissions-profile' ), + $this->_fs->get_text_inline( 'Your WordPress user\'s: first & last name, and email address', 'permissions-profile_desc' ), + $this->_fs->get_text_inline( 'Never miss important updates, get security warnings before they become public knowledge, and receive notifications about special offers and awesome new features.', 'permissions-profile_tooltip' ), + 5, + false, + true, + $load_default_from_storage + ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Optional Permissions + #-------------------------------------------------------------------------------- + + /** + * @return array[] + */ + function get_newsletter_permission() { + return $this->get_permission( + self::PERMISSION_NEWSLETTER, + 'email-alt', + $this->_fs->get_text_inline( 'Newsletter', 'permissions-newsletter' ), + $this->_fs->get_text_inline( 'Updates, announcements, marketing, no spam', 'permissions-newsletter_desc' ), + '', + 15 + ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Permissions Storage + #-------------------------------------------------------------------------------- + + /** + * @param int|null $blog_id + * + * @return bool + */ + function is_extensions_tracking_allowed( $blog_id = null ) { + return $this->is_permission_allowed( self::PERMISSION_EXTENSIONS, ! $this->_fs->is_premium(), $blog_id ); + } + + /** + * @param int|null $blog_id + * + * @return bool + */ + function is_essentials_tracking_allowed( $blog_id = null ) { + return $this->is_permission_allowed( self::PERMISSION_ESSENTIALS, true, $blog_id ); + } + + /** + * @param bool $default + * + * @return bool + */ + function is_diagnostic_tracking_allowed( $default = true ) { + return $this->is_premium_context() ? + $this->is_permission_allowed( self::PERMISSION_DIAGNOSTIC, $default ) : + $this->is_permission_allowed( self::PERMISSION_SITE, $default ); + } + + /** + * @param int|null $blog_id + * + * @return bool + */ + function is_homepage_url_tracking_allowed( $blog_id = null ) { + return $this->is_permission_allowed( $this->get_site_permission_name(), true, $blog_id ); + } + + /** + * @param int|null $blog_id + * + * @return bool + */ + function update_site_tracking( $is_enabled, $blog_id = null, $only_if_not_set = false ) { + $permissions = $this->get_site_tracking_permission_names(); + + $result = true; + foreach ( $permissions as $permission ) { + if ( ! $only_if_not_set || ! $this->is_permission_set( $permission, $blog_id ) ) { + $result = ( $result && $this->update_permission_tracking_flag( $permission, $is_enabled, $blog_id ) ); + } + } + + return $result; + } + + /** + * @param string $permission + * @param bool $default + * @param int|null $blog_id + * + * @return bool + */ + function is_permission_allowed( $permission, $default = false, $blog_id = null ) { + if ( ! self::is_supported_permission( $permission ) ) { + return $default; + } + + return $this->is_permission( $permission, true, $blog_id ); + } + + /** + * @param string $permission + * @param bool $is_allowed + * @param int|null $blog_id + * + * @return bool + */ + function is_permission( $permission, $is_allowed, $blog_id = null ) { + if ( ! self::is_supported_permission( $permission ) ) { + return false; + } + + $tag = "is_{$permission}_tracking_allowed"; + + return ( $is_allowed === $this->_fs->apply_filters( + $tag, + $this->_storage->get( + $tag, + $this->get_permission_default( $permission ), + $blog_id, + FS_Storage::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED + ) + ) ); + } + + /** + * @param string $permission + * @param int|null $blog_id + * + * @return bool + */ + function is_permission_set( $permission, $blog_id = null ) { + $tag = "is_{$permission}_tracking_allowed"; + + $permission = $this->_storage->get( + $tag, + null, + $blog_id, + FS_Storage::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED + ); + + return is_bool( $permission ); + } + + /** + * @param string[] $permissions + * @param bool $is_allowed + * + * @return bool `true` if all given permissions are in sync with `$is_allowed`. + */ + function are_permissions( $permissions, $is_allowed, $blog_id = null ) { + foreach ( $permissions as $permission ) { + if ( ! $this->is_permission( $permission, $is_allowed, $blog_id ) ) { + return false; + } + } + + return true; + } + + /** + * @param string $permission + * @param bool $is_enabled + * @param int|null $blog_id + * + * @return bool `false` if permission not supported or `$is_enabled` is not a boolean. + */ + function update_permission_tracking_flag( $permission, $is_enabled, $blog_id = null ) { + if ( is_bool( $is_enabled ) && self::is_supported_permission( $permission ) ) { + $this->_storage->store( + "is_{$permission}_tracking_allowed", + $is_enabled, + $blog_id, + FS_Storage::OPTION_LEVEL_NETWORK_ACTIVATED_NOT_DELEGATED + ); + + return true; + } + + return false; + } + + /** + * @param array $permissions + */ + function update_permissions_tracking_flag( $permissions ) { + foreach ( $permissions as $permission => $is_enabled ) { + $this->update_permission_tracking_flag( $permission, $is_enabled ); + } + } + + #endregion + + + /** + * @param string $permission + * + * @return bool + */ + function get_permission_default( $permission ) { + if ( + $this->_fs->is_premium() && + self::PERMISSION_EXTENSIONS === $permission + ) { + return false; + } + + // All permissions except for the extensions in paid version are on by default when the user opts in to usage tracking. + return true; + } + + /** + * @return string + */ + function get_site_permission_name() { + return $this->is_premium_context() ? + self::PERMISSION_ESSENTIALS : + self::PERMISSION_SITE; + } + + /** + * @return string[] + */ + function get_site_tracking_permission_names() { + return $this->is_premium_context() ? + array( + FS_Permission_Manager::PERMISSION_ESSENTIALS, + FS_Permission_Manager::PERMISSION_EVENTS, + ) : + array( FS_Permission_Manager::PERMISSION_SITE ); + } + + #-------------------------------------------------------------------------------- + #region Rendering + #-------------------------------------------------------------------------------- + + /** + * @param array $permission + */ + function render_permission( array $permission ) { + fs_require_template( 'connect/permission.php', $permission ); + } + + /** + * @param array $permissions_group + */ + function render_permissions_group( array $permissions_group ) { + $permissions_group[ 'fs' ] = $this->_fs; + + fs_require_template( 'connect/permissions-group.php', $permissions_group ); + } + + function require_permissions_js() { + fs_require_once_template( 'js/permissions.php', $params ); + } + + #endregion + + #-------------------------------------------------------------------------------- + #region Helper Methods + #-------------------------------------------------------------------------------- + + /** + * @param string $id + * @param string $dashicon + * @param string $label + * @param string $desc + * @param string $tooltip + * @param int $priority + * @param bool $is_optional + * @param bool $is_on_by_default + * @param bool $load_from_storage + * + * @return array + */ + private function get_permission( + $id, + $dashicon, + $label, + $desc, + $tooltip = '', + $priority = 10, + $is_optional = false, + $is_on_by_default = true, + $load_from_storage = false + ) { + $is_on = $load_from_storage ? + $this->is_permission_allowed( $id, $is_on_by_default ) : + $is_on_by_default; + + return array( + 'id' => $id, + 'icon-class' => $this->_fs->apply_filters( "permission_{$id}_icon", "dashicons dashicons-{$dashicon}" ), + 'label' => $this->_fs->apply_filters( "permission_{$id}_label", $label ), + 'tooltip' => $this->_fs->apply_filters( "permission_{$id}_tooltip", $tooltip ), + 'desc' => $this->_fs->apply_filters( "permission_{$id}_desc", $desc ), + 'priority' => $this->_fs->apply_filters( "permission_{$id}_priority", $priority ), + 'optional' => $is_optional, + 'default' => $this->_fs->apply_filters( "permission_{$id}_default", $is_on ), + ); + } + + /** + * @param array $permissions + * + * @return array[] + */ + private function get_sorted_permissions_by_priority( array $permissions ) { + // Allow filtering of the permissions list. + $permissions = $this->_fs->apply_filters( 'permission_list', $permissions ); + + // Sort by priority. + uasort( $permissions, 'fs_sort_by_priority' ); + + return $permissions; + } + + #endregion + } \ No newline at end of file diff --git a/freemius/includes/managers/class-fs-plugin-manager.php b/freemius/includes/managers/class-fs-plugin-manager.php index bacf160..bafca67 100644 --- a/freemius/includes/managers/class-fs-plugin-manager.php +++ b/freemius/includes/managers/class-fs-plugin-manager.php @@ -211,10 +211,23 @@ function set( FS_Plugin $plugin, $store = false ) { * @return bool|\FS_Plugin */ function get() { - return isset( $this->_module ) ? - $this->_module : - false; + if ( isset( $this->_module ) ) { + return $this->_module; + } + + if ( empty( $this->_module_id ) ) { + return false; + } + + /** + * Return an FS_Plugin entity that has its `id` and `is_live` properties set (`is_live` is initialized in the FS_Plugin constructor) to avoid triggering an error that is relevant to these properties when the FS_Plugin entity is used before the `parse_settings()` method is called. This can happen when creating a regular WordPress site by cloning a subsite of a multisite network and the data that is stored in the network-level storage is not cloned. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + $plugin = new FS_Plugin(); + $plugin->id = $this->_module_id; + + return $plugin; } - - } \ No newline at end of file diff --git a/freemius/includes/sdk/FreemiusBase.php b/freemius/includes/sdk/FreemiusBase.php index a486e90..42ecbc2 100644 --- a/freemius/includes/sdk/FreemiusBase.php +++ b/freemius/includes/sdk/FreemiusBase.php @@ -46,174 +46,172 @@ require_once FS_SDK__EXCEPTIONS_PATH . $e . '.php'; } - if ( class_exists( 'Freemius_Api_Base' ) ) { - return; - } - - abstract class Freemius_Api_Base { - const VERSION = '1.0.4'; - const FORMAT = 'json'; - - protected $_id; - protected $_public; - protected $_secret; - protected $_scope; - protected $_isSandbox; - - /** - * @param string $pScope 'app', 'developer', 'plugin', 'user' or 'install'. - * @param number $pID Element's id. - * @param string $pPublic Public key. - * @param string $pSecret Element's secret key. - * @param bool $pIsSandbox Whether or not to run API in sandbox mode. - */ - public function Init( $pScope, $pID, $pPublic, $pSecret, $pIsSandbox = false ) { - $this->_id = $pID; - $this->_public = $pPublic; - $this->_secret = $pSecret; - $this->_scope = $pScope; - $this->_isSandbox = $pIsSandbox; - } - - public function IsSandbox() { - return $this->_isSandbox; - } - - function CanonizePath( $pPath ) { - $pPath = trim( $pPath, '/' ); - $query_pos = strpos( $pPath, '?' ); - $query = ''; - - if ( false !== $query_pos ) { - $query = substr( $pPath, $query_pos ); - $pPath = substr( $pPath, 0, $query_pos ); - } - - // Trim '.json' suffix. - $format_length = strlen( '.' . self::FORMAT ); - $start = $format_length * ( - 1 ); //negative - if ( substr( strtolower( $pPath ), $start ) === ( '.' . self::FORMAT ) ) { - $pPath = substr( $pPath, 0, strlen( $pPath ) - $format_length ); - } - - switch ( $this->_scope ) { - case 'app': - $base = '/apps/' . $this->_id; - break; - case 'developer': - $base = '/developers/' . $this->_id; - break; - case 'user': - $base = '/users/' . $this->_id; - break; - case 'plugin': - $base = '/plugins/' . $this->_id; - break; - case 'install': - $base = '/installs/' . $this->_id; - break; - default: - throw new Freemius_Exception( 'Scope not implemented.' ); - } - - return '/v' . FS_API__VERSION . $base . - ( ! empty( $pPath ) ? '/' : '' ) . $pPath . - ( ( false === strpos( $pPath, '.' ) ) ? '.' . self::FORMAT : '' ) . $query; - } - - abstract function MakeRequest( $pCanonizedPath, $pMethod = 'GET', $pParams = array() ); - - /** - * @param string $pPath - * @param string $pMethod - * @param array $pParams - * - * @return object[]|object|null - */ - private function _Api( $pPath, $pMethod = 'GET', $pParams = array() ) { - $pMethod = strtoupper( $pMethod ); - - try { - $result = $this->MakeRequest( $pPath, $pMethod, $pParams ); - } catch ( Freemius_Exception $e ) { - // Map to error object. - $result = (object) $e->getResult(); - } catch ( Exception $e ) { - // Map to error object. - $result = (object) array( - 'error' => (object) array( - 'type' => 'Unknown', - 'message' => $e->getMessage() . ' (' . $e->getFile() . ': ' . $e->getLine() . ')', - 'code' => 'unknown', - 'http' => 402 - ) - ); - } - - return $result; - } - - public function Api( $pPath, $pMethod = 'GET', $pParams = array() ) { - return $this->_Api( $this->CanonizePath( $pPath ), $pMethod, $pParams ); - } - - /** - * Base64 decoding that does not need to be urldecode()-ed. - * - * Exactly the same as PHP base64 encode except it uses - * `-` instead of `+` - * `_` instead of `/` - * No padded = - * - * @param string $input Base64UrlEncoded() string - * - * @return string - */ - protected static function Base64UrlDecode( $input ) { - /** - * IMPORTANT NOTE: - * This is a hack suggested by @otto42 and @greenshady from - * the theme's review team. The usage of base64 for API - * signature encoding was approved in a Slack meeting - * held on Tue (10/25 2016). - * - * @todo Remove this hack once the base64 error is removed from the Theme Check. - * - * @since 1.2.2 - * @author Vova Feldman (@svovaf) - */ - $fn = 'base64' . '_decode'; - return $fn( strtr( $input, '-_', '+/' ) ); - } - - /** - * Base64 encoding that does not need to be urlencode()ed. - * - * Exactly the same as base64 encode except it uses - * `-` instead of `+ - * `_` instead of `/` - * - * @param string $input string - * - * @return string Base64 encoded string - */ - protected static function Base64UrlEncode( $input ) { - /** - * IMPORTANT NOTE: - * This is a hack suggested by @otto42 and @greenshady from - * the theme's review team. The usage of base64 for API - * signature encoding was approved in a Slack meeting - * held on Tue (10/25 2016). - * - * @todo Remove this hack once the base64 error is removed from the Theme Check. - * - * @since 1.2.2 - * @author Vova Feldman (@svovaf) - */ - $fn = 'base64' . '_encode'; - $str = strtr( $fn( $input ), '+/', '-_' ); - $str = str_replace( '=', '', $str ); - - return $str; - } - } + if ( ! class_exists( 'Freemius_Api_Base' ) ) { + abstract class Freemius_Api_Base { + const VERSION = '1.0.4'; + const FORMAT = 'json'; + + protected $_id; + protected $_public; + protected $_secret; + protected $_scope; + protected $_isSandbox; + + /** + * @param string $pScope 'app', 'developer', 'plugin', 'user' or 'install'. + * @param number $pID Element's id. + * @param string $pPublic Public key. + * @param string $pSecret Element's secret key. + * @param bool $pIsSandbox Whether or not to run API in sandbox mode. + */ + public function Init( $pScope, $pID, $pPublic, $pSecret, $pIsSandbox = false ) { + $this->_id = $pID; + $this->_public = $pPublic; + $this->_secret = $pSecret; + $this->_scope = $pScope; + $this->_isSandbox = $pIsSandbox; + } + + public function IsSandbox() { + return $this->_isSandbox; + } + + function CanonizePath( $pPath ) { + $pPath = trim( $pPath, '/' ); + $query_pos = strpos( $pPath, '?' ); + $query = ''; + + if ( false !== $query_pos ) { + $query = substr( $pPath, $query_pos ); + $pPath = substr( $pPath, 0, $query_pos ); + } + + // Trim '.json' suffix. + $format_length = strlen( '.' . self::FORMAT ); + $start = $format_length * ( - 1 ); //negative + if ( substr( strtolower( $pPath ), $start ) === ( '.' . self::FORMAT ) ) { + $pPath = substr( $pPath, 0, strlen( $pPath ) - $format_length ); + } + + switch ( $this->_scope ) { + case 'app': + $base = '/apps/' . $this->_id; + break; + case 'developer': + $base = '/developers/' . $this->_id; + break; + case 'user': + $base = '/users/' . $this->_id; + break; + case 'plugin': + $base = '/plugins/' . $this->_id; + break; + case 'install': + $base = '/installs/' . $this->_id; + break; + default: + throw new Freemius_Exception( 'Scope not implemented.' ); + } + + return '/v' . FS_API__VERSION . $base . + ( ! empty( $pPath ) ? '/' : '' ) . $pPath . + ( ( false === strpos( $pPath, '.' ) ) ? '.' . self::FORMAT : '' ) . $query; + } + + abstract function MakeRequest( $pCanonizedPath, $pMethod = 'GET', $pParams = array() ); + + /** + * @param string $pPath + * @param string $pMethod + * @param array $pParams + * + * @return object[]|object|null + */ + private function _Api( $pPath, $pMethod = 'GET', $pParams = array() ) { + $pMethod = strtoupper( $pMethod ); + + try { + $result = $this->MakeRequest( $pPath, $pMethod, $pParams ); + } catch ( Freemius_Exception $e ) { + // Map to error object. + $result = (object) $e->getResult(); + } catch ( Exception $e ) { + // Map to error object. + $result = (object) array( + 'error' => (object) array( + 'type' => 'Unknown', + 'message' => $e->getMessage() . ' (' . $e->getFile() . ': ' . $e->getLine() . ')', + 'code' => 'unknown', + 'http' => 402 + ) + ); + } + + return $result; + } + + public function Api( $pPath, $pMethod = 'GET', $pParams = array() ) { + return $this->_Api( $this->CanonizePath( $pPath ), $pMethod, $pParams ); + } + + /** + * Base64 decoding that does not need to be urldecode()-ed. + * + * Exactly the same as PHP base64 encode except it uses + * `-` instead of `+` + * `_` instead of `/` + * No padded = + * + * @param string $input Base64UrlEncoded() string + * + * @return string + */ + protected static function Base64UrlDecode( $input ) { + /** + * IMPORTANT NOTE: + * This is a hack suggested by @otto42 and @greenshady from + * the theme's review team. The usage of base64 for API + * signature encoding was approved in a Slack meeting + * held on Tue (10/25 2016). + * + * @todo Remove this hack once the base64 error is removed from the Theme Check. + * + * @since 1.2.2 + * @author Vova Feldman (@svovaf) + */ + $fn = 'base64' . '_decode'; + return $fn( strtr( $input, '-_', '+/' ) ); + } + + /** + * Base64 encoding that does not need to be urlencode()ed. + * + * Exactly the same as base64 encode except it uses + * `-` instead of `+ + * `_` instead of `/` + * + * @param string $input string + * + * @return string Base64 encoded string + */ + protected static function Base64UrlEncode( $input ) { + /** + * IMPORTANT NOTE: + * This is a hack suggested by @otto42 and @greenshady from + * the theme's review team. The usage of base64 for API + * signature encoding was approved in a Slack meeting + * held on Tue (10/25 2016). + * + * @todo Remove this hack once the base64 error is removed from the Theme Check. + * + * @since 1.2.2 + * @author Vova Feldman (@svovaf) + */ + $fn = 'base64' . '_encode'; + $str = strtr( $fn( $input ), '+/', '-_' ); + $str = str_replace( '=', '', $str ); + + return $str; + } + } + } \ No newline at end of file diff --git a/freemius/includes/sdk/FreemiusWordPress.php b/freemius/includes/sdk/FreemiusWordPress.php index 354273d..efb9e0d 100644 --- a/freemius/includes/sdk/FreemiusWordPress.php +++ b/freemius/includes/sdk/FreemiusWordPress.php @@ -85,10 +85,7 @@ define( 'FS_API__SANDBOX_ADDRESS', '://sandbox-api.freemius.com' ); } - if ( class_exists( 'Freemius_Api_WordPress' ) ) { - return; - } - + if ( ! class_exists( 'Freemius_Api_WordPress' ) ) { class Freemius_Api_WordPress extends Freemius_Api_Base { private static $_logger = array(); @@ -167,6 +164,15 @@ public static function SetHttp() { self::$_protocol = 'http'; } + /** + * Sets API connection protocol to HTTPS. + * + * @since 2.5.4 + */ + public static function SetHttps() { + self::$_protocol = 'https'; + } + /** * @since 1.0.4 * @@ -305,9 +311,11 @@ function GetSignedUrl( $pPath ) { * @return mixed */ private static function ExecuteRequest( $pUrl, &$pWPRemoteArgs ) { + $bt = debug_backtrace(); + $start = microtime( true ); - $response = wp_remote_request( $pUrl, $pWPRemoteArgs ); + $response = self::RemoteRequest( $pUrl, $pWPRemoteArgs ); if ( FS_API__LOGGER_ON ) { $end = microtime( true ); @@ -327,13 +335,38 @@ private static function ExecuteRequest( $pUrl, &$pWPRemoteArgs ) { $response['body'] : json_encode( $response->get_error_messages() ), 'code' => ! $is_http_error ? $response['response']['code'] : null, - 'backtrace' => debug_backtrace(), + 'backtrace' => $bt, ); } return $response; } + /** + * @author Leo Fajardo (@leorw) + * + * @param string $pUrl + * @param array $pWPRemoteArgs + * + * @return array|WP_Error The response array or a WP_Error on failure. + */ + static function RemoteRequest( $pUrl, $pWPRemoteArgs ) { + $response = wp_remote_request( $pUrl, $pWPRemoteArgs ); + + if ( + is_array( $response ) && + ( + empty( $response['headers'] ) || + empty( $response['headers']['x-api-server'] ) + ) + ) { + // API is considered blocked if the response doesn't include the `x-api-server` header. When there's no error but this header doesn't exist, the response is usually not in the expected form (e.g., cannot be JSON-decoded). + $response = new WP_Error( 'api_blocked', htmlentities( $response['body'] ) ); + } + + return $response; + } + /** * @return array */ @@ -544,28 +577,20 @@ static function CurlResolveToIPv4( $handle ) { #region Connectivity Test #---------------------------------------------------------------------------------- - /** - * If successful connectivity to the API endpoint using ping.json endpoint. - * - * - OR - - * - * Validate if ping result object is valid. - * - * @param mixed $pPong - * - * @return bool - */ - public static function Test( $pPong = null ) { - $pong = is_null( $pPong ) ? - self::Ping() : - $pPong; - - return ( - is_object( $pong ) && - isset( $pong->api ) && - 'pong' === $pong->api - ); - } + /** + * This method exists only for backward compatibility to prevent a fatal error from happening when called from an outdated piece of code. + * + * @param mixed $pPong + * + * @return bool + */ + public static function Test( $pPong = null ) { + return ( + is_object( $pPong ) && + isset( $pPong->api ) && + 'pong' === $pPong->api + ); + } /** * Ping API to test connectivity. @@ -713,3 +738,4 @@ private static function ThrowSquidAclException( $pResult = '' ) { #endregion } + } diff --git a/freemius/includes/supplements/fs-migration-2.5.1.php b/freemius/includes/supplements/fs-migration-2.5.1.php new file mode 100644 index 0000000..6252649 --- /dev/null +++ b/freemius/includes/supplements/fs-migration-2.5.1.php @@ -0,0 +1,31 @@ + $install ) { + if ( true === $install->is_disconnected ) { + $permission_manager->update_site_tracking( + false, + ( 0 == $blog_id ) ? null : $blog_id, + // Update only if permissions are not yet set. + true + ); + } + } + } + } \ No newline at end of file diff --git a/freemius/languages/freemius-cs_CZ.mo b/freemius/languages/freemius-cs_CZ.mo index 278bd9f..8dd3c06 100644 Binary files a/freemius/languages/freemius-cs_CZ.mo and b/freemius/languages/freemius-cs_CZ.mo differ diff --git a/freemius/languages/freemius-da_DK.mo b/freemius/languages/freemius-da_DK.mo index f674098..07ffb83 100644 Binary files a/freemius/languages/freemius-da_DK.mo and b/freemius/languages/freemius-da_DK.mo differ diff --git a/freemius/languages/freemius-de_DE.mo b/freemius/languages/freemius-de_DE.mo index 879edb7..2d951e9 100644 Binary files a/freemius/languages/freemius-de_DE.mo and b/freemius/languages/freemius-de_DE.mo differ diff --git a/freemius/languages/freemius-en.mo b/freemius/languages/freemius-en.mo index c2d4990..cecd3e5 100644 Binary files a/freemius/languages/freemius-en.mo and b/freemius/languages/freemius-en.mo differ diff --git a/freemius/languages/freemius-es_ES.mo b/freemius/languages/freemius-es_ES.mo index a887605..445e10d 100644 Binary files a/freemius/languages/freemius-es_ES.mo and b/freemius/languages/freemius-es_ES.mo differ diff --git a/freemius/languages/freemius-fr_FR.mo b/freemius/languages/freemius-fr_FR.mo index 662725d..bc2889c 100644 Binary files a/freemius/languages/freemius-fr_FR.mo and b/freemius/languages/freemius-fr_FR.mo differ diff --git a/freemius/languages/freemius-he_IL.mo b/freemius/languages/freemius-he_IL.mo index 8bbafc3..5b4b235 100644 Binary files a/freemius/languages/freemius-he_IL.mo and b/freemius/languages/freemius-he_IL.mo differ diff --git a/freemius/languages/freemius-hu_HU.mo b/freemius/languages/freemius-hu_HU.mo index d9b9ff4..bd75896 100644 Binary files a/freemius/languages/freemius-hu_HU.mo and b/freemius/languages/freemius-hu_HU.mo differ diff --git a/freemius/languages/freemius-it_IT.mo b/freemius/languages/freemius-it_IT.mo index be8d837..6646907 100644 Binary files a/freemius/languages/freemius-it_IT.mo and b/freemius/languages/freemius-it_IT.mo differ diff --git a/freemius/languages/freemius-ja.mo b/freemius/languages/freemius-ja.mo index 27b73f3..6acf2df 100644 Binary files a/freemius/languages/freemius-ja.mo and b/freemius/languages/freemius-ja.mo differ diff --git a/freemius/languages/freemius-nl_NL.mo b/freemius/languages/freemius-nl_NL.mo index 0bb243c..b3024ef 100644 Binary files a/freemius/languages/freemius-nl_NL.mo and b/freemius/languages/freemius-nl_NL.mo differ diff --git a/freemius/languages/freemius-ru_RU.mo b/freemius/languages/freemius-ru_RU.mo index ae56051..c4982fd 100644 Binary files a/freemius/languages/freemius-ru_RU.mo and b/freemius/languages/freemius-ru_RU.mo differ diff --git a/freemius/languages/freemius-ta.mo b/freemius/languages/freemius-ta.mo index 0d0fb5f..0fbe1eb 100644 Binary files a/freemius/languages/freemius-ta.mo and b/freemius/languages/freemius-ta.mo differ diff --git a/freemius/languages/freemius-zh_CN.mo b/freemius/languages/freemius-zh_CN.mo index 48ee0cc..6ac0918 100644 Binary files a/freemius/languages/freemius-zh_CN.mo and b/freemius/languages/freemius-zh_CN.mo differ diff --git a/freemius/languages/freemius.pot b/freemius/languages/freemius.pot index 209d7ed..3bdefb1 100644 --- a/freemius/languages/freemius.pot +++ b/freemius/languages/freemius.pot @@ -1,4 +1,4 @@ -# Copyright (C) 2022 freemius +# Copyright (C) 2023 freemius # This file is distributed under the same license as the freemius package. msgid "" msgstr "" @@ -8,7 +8,6 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Language-Team: Freemius Team \n" "Last-Translator: Vova Feldman \n" -"POT-Creation-Date: 2022-07-06 12:49+0000\n" "Report-Msgid-Bugs-To: https://github.com/Freemius/wordpress-sdk/issues\n" "X-Poedit-Basepath: ..\n" "X-Poedit-KeywordsList: get_text_inline;fs_text_inline;fs_echo_inline;fs_esc_js_inline;fs_esc_attr_inline;fs_esc_attr_echo_inline;fs_esc_html_inline;fs_esc_html_echo_inline;get_text_x_inline:1,2c;fs_text_x_inline:1,2c;fs_echo_x_inline:1,2c;fs_esc_attr_x_inline:1,2c;fs_esc_js_x_inline:1,2c;fs_esc_js_echo_x_inline:1,2c;fs_esc_html_x_inline:1,2c;fs_esc_html_echo_x_inline:1,2c\n" @@ -17,687 +16,1454 @@ msgstr "" "X-Poedit-SourceCharset: UTF-8\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: includes/class-freemius.php:1932, templates/account.php:941 +#: includes/class-freemius.php:1748, templates/account.php:947 msgid "An update to a Beta version will replace your installed version of %s with the latest Beta release - use with caution, and not on production sites. You have been warned." msgstr "" -#: includes/class-freemius.php:1939 +#: includes/class-freemius.php:1755 msgid "Would you like to proceed with the update?" msgstr "" -#: includes/class-freemius.php:3751, templates/debug.php:20 +#: includes/class-freemius.php:1980 +msgid "Freemius SDK couldn't find the plugin's main file. Please contact sdk@freemius.com with the current error." +msgstr "" + +#: includes/class-freemius.php:1982, includes/fs-plugin-info-dialog.php:1517 +msgid "Error" +msgstr "" + +#: includes/class-freemius.php:2428 +msgid "I found a better %s" +msgstr "" + +#: includes/class-freemius.php:2430 +msgid "What's the %s's name?" +msgstr "" + +#: includes/class-freemius.php:2436 +msgid "It's a temporary %s - I'm troubleshooting an issue" +msgstr "" + +#: includes/class-freemius.php:2438 +msgid "Deactivation" +msgstr "" + +#: includes/class-freemius.php:2439 +msgid "Theme Switch" +msgstr "" + +#: includes/class-freemius.php:2448, templates/forms/resend-key.php:24, templates/forms/user-change.php:29 +msgid "Other" +msgstr "" + +#: includes/class-freemius.php:2456 +msgid "I no longer need the %s" +msgstr "" + +#: includes/class-freemius.php:2463 +msgid "I only needed the %s for a short period" +msgstr "" + +#: includes/class-freemius.php:2469 +msgid "The %s broke my site" +msgstr "" + +#: includes/class-freemius.php:2476 +msgid "The %s suddenly stopped working" +msgstr "" + +#: includes/class-freemius.php:2486 +msgid "I can't pay for it anymore" +msgstr "" + +#: includes/class-freemius.php:2488 +msgid "What price would you feel comfortable paying?" +msgstr "" + +#: includes/class-freemius.php:2494 +msgid "I don't like to share my information with you" +msgstr "" + +#: includes/class-freemius.php:2515 +msgid "The %s didn't work" +msgstr "" + +#: includes/class-freemius.php:2525 +msgid "I couldn't understand how to make it work" +msgstr "" + +#: includes/class-freemius.php:2533 +msgid "The %s is great, but I need specific feature that you don't support" +msgstr "" + +#: includes/class-freemius.php:2535 +msgid "What feature?" +msgstr "" + +#: includes/class-freemius.php:2539 +msgid "The %s is not working" +msgstr "" + +#: includes/class-freemius.php:2541 +msgid "Kindly share what didn't work so we can fix it for future users..." +msgstr "" + +#: includes/class-freemius.php:2545 +msgid "It's not what I was looking for" +msgstr "" + +#: includes/class-freemius.php:2547 +msgid "What you've been looking for?" +msgstr "" + +#: includes/class-freemius.php:2551 +msgid "The %s didn't work as expected" +msgstr "" + +#: includes/class-freemius.php:2553 +msgid "What did you expect?" +msgstr "" + +#: includes/class-freemius.php:3641, templates/debug.php:24 msgid "Freemius Debug" msgstr "" -#: includes/class-freemius.php:13791 +#: includes/class-freemius.php:4755 +msgid "You have purchased a %s license." +msgstr "" + +#: includes/class-freemius.php:4759 +msgid " The %s's %sdownload link%s, license key, and installation instructions have been sent to %s. If you can't find the email after 5 min, please check your spam box." +msgstr "" + +#: includes/class-freemius.php:4769, includes/class-freemius.php:21125, includes/class-freemius.php:24783 +msgctxt "interjection expressing joy or exuberance" +msgid "Yee-haw" +msgstr "" + +#: includes/class-freemius.php:4783 +msgctxt "addonX cannot run without pluginY" +msgid "%s cannot run without %s." +msgstr "" + +#: includes/class-freemius.php:4784 +msgctxt "addonX cannot run..." +msgid "%s cannot run without the plugin." +msgstr "" + +#: includes/class-freemius.php:4786, includes/class-freemius.php:5978, includes/class-freemius.php:13730, includes/class-freemius.php:14469, includes/class-freemius.php:18281, includes/class-freemius.php:18394, includes/class-freemius.php:18571, includes/class-freemius.php:20856, includes/class-freemius.php:21955, includes/class-freemius.php:22971, includes/class-freemius.php:23101, includes/class-freemius.php:23231, templates/add-ons.php:57 +msgctxt "exclamation" +msgid "Oops" +msgstr "" + +#: includes/class-freemius.php:5065 +msgid "There was an unexpected API error while processing your request. Please try again in a few minutes and if it still doesn't work, contact the %s's author with the following:" +msgstr "" + +#: includes/class-freemius.php:5645 +msgid "Premium %s version was successfully activated." +msgstr "" + +#: includes/class-freemius.php:5657, includes/class-freemius.php:7692 +msgctxt "Used to express elation, enthusiasm, or triumph (especially in electronic communication)." +msgid "W00t" +msgstr "" + +#: includes/class-freemius.php:5672 +msgid "You have a %s license." +msgstr "" + +#: includes/class-freemius.php:5961 +msgid "%s free trial was successfully cancelled. Since the add-on is premium only it was automatically deactivated. If you like to use it in the future, you'll have to purchase a license." +msgstr "" + +#: includes/class-freemius.php:5965 +msgid "%s is a premium only add-on. You have to purchase a license first before activating the plugin." +msgstr "" + +#: includes/class-freemius.php:5974, templates/add-ons.php:186, templates/account/partials/addon.php:386 +msgid "More information about %s" +msgstr "" + +#: includes/class-freemius.php:5975 +msgid "Purchase License" +msgstr "" + +#. translators: %3$s: action (e.g.: "start the trial" or "complete the opt-in") +#: includes/class-freemius.php:6971 +msgid "You should receive a confirmation email for %1$s to your mailbox at %2$s. Please make sure you click the button in that email to %3$s." +msgstr "" + +#: includes/class-freemius.php:6974 +msgid "start the trial" +msgstr "" + +#: includes/class-freemius.php:6975, templates/connect.php:218 +msgid "complete the opt-in" +msgstr "" + +#: includes/class-freemius.php:6977 +msgid "Thanks!" +msgstr "" + +#. translators: %3$s: What the user is expected to receive via email (e.g.: "the installation instructions" or "a license key") +#: includes/class-freemius.php:6980 +msgid "You should receive %3$s for %1$s to your mailbox at %2$s in the next 5 minutes." +msgstr "" + +#: includes/class-freemius.php:6983 +msgctxt "Part of the message telling the user what they should receive via email." +msgid "the installation instructions" +msgstr "" + +#: includes/class-freemius.php:6989 +msgctxt "Part of the message telling the user what they should receive via email." +msgid "a license key" +msgstr "" + +#: includes/class-freemius.php:6997 +msgid "%s to activate the license once you get it." +msgstr "" + +#: includes/class-freemius.php:7005 +msgctxt "Part of an activation link message." +msgid "Click here" +msgstr "" + +#: includes/class-freemius.php:7012 +msgctxt "Part of the message that tells the user to check their spam folder for a specific email." +msgid "the product's support email address" +msgstr "" + +#: includes/class-freemius.php:7018 +msgid "If you didn't get the email, try checking your spam folder or search for emails from %4$s." +msgstr "" + +#: includes/class-freemius.php:7020 +msgid "Thanks for upgrading." +msgstr "" + +#: includes/class-freemius.php:7156 +msgid "You are just one step away - %s" +msgstr "" + +#: includes/class-freemius.php:7159 +msgctxt "%s - plugin name. As complete \"PluginX\" activation now" +msgid "Complete \"%s\" Activation Now" +msgstr "" + +#: includes/class-freemius.php:7241 +msgid "We made a few tweaks to the %s, %s" +msgstr "" + +#: includes/class-freemius.php:7245 +msgid "Opt in to make \"%s\" better!" +msgstr "" + +#: includes/class-freemius.php:7691 +msgid "The upgrade of %s was successfully completed." +msgstr "" + +#: includes/class-freemius.php:10441, includes/class-fs-plugin-updater.php:1100, includes/class-fs-plugin-updater.php:1315, includes/class-fs-plugin-updater.php:1322, templates/auto-installation.php:32 +msgid "Add-On" +msgstr "" + +#: includes/class-freemius.php:10443, templates/account.php:411, templates/account.php:419, templates/debug.php:399, templates/debug.php:619 +msgid "Plugin" +msgstr "" + +#: includes/class-freemius.php:10444, templates/account.php:412, templates/account.php:420, templates/debug.php:399, templates/debug.php:619, templates/forms/deactivation/form.php:107 +msgid "Theme" +msgstr "" + +#: includes/class-freemius.php:13549 msgid "An unknown error has occurred while trying to toggle the license's white-label mode." msgstr "" -#: includes/class-freemius.php:13869 +#: includes/class-freemius.php:13563 +msgid "Your %s license was flagged as white-labeled to hide sensitive information from the WP Admin (e.g. your email, license key, prices, billing address & invoices). If you ever wish to revert it back, you can easily do it through your %s. If this was a mistake you can also %s." +msgstr "" + +#: includes/class-freemius.php:13568, templates/account/partials/disconnect-button.php:84 +msgid "User Dashboard" +msgstr "" + +#: includes/class-freemius.php:13569 +msgid "revert it now" +msgstr "" + +#: includes/class-freemius.php:13627 msgid "An unknown error has occurred while trying to set the user's beta mode." msgstr "" -#: includes/class-freemius.php:13942 +#: includes/class-freemius.php:13701 msgid "Invalid new user ID or email address." msgstr "" -#: includes/class-freemius.php:23326 +#: includes/class-freemius.php:13731 +msgid "Sorry, we could not complete the email update. Another user with the same email is already registered." +msgstr "" + +#: includes/class-freemius.php:13732 +msgid "If you would like to give up the ownership of the %s's account to %s click the Change Ownership button." +msgstr "" + +#: includes/class-freemius.php:13739 +msgid "Change Ownership" +msgstr "" + +#: includes/class-freemius.php:14336 +msgid "Invalid site details collection." +msgstr "" + +#: includes/class-freemius.php:14456 +msgid "We couldn't find your email address in the system, are you sure it's the right address?" +msgstr "" + +#: includes/class-freemius.php:14458 +msgid "We can't see any active licenses associated with that email address, are you sure it's the right address?" +msgstr "" + +#: includes/class-freemius.php:14756 +msgid "Account is pending activation. Please check your email and click the link to activate your account and then submit the affiliate form again." +msgstr "" + +#: includes/class-freemius.php:14870, templates/forms/premium-versions-upgrade-handler.php:47 +msgid "Buy a license now" +msgstr "" + +#: includes/class-freemius.php:14882, templates/forms/premium-versions-upgrade-handler.php:46 +msgid "Renew your license now" +msgstr "" + +#: includes/class-freemius.php:14886 +msgid "%s to access version %s security & feature updates, and support." +msgstr "" + +#: includes/class-freemius.php:17621 +msgid "%s opt-in was successfully completed." +msgstr "" + +#: includes/class-freemius.php:17635 +msgid "Your account was successfully activated with the %s plan." +msgstr "" + +#: includes/class-freemius.php:17645, includes/class-freemius.php:21566 +msgid "Your trial has been successfully started." +msgstr "" + +#: includes/class-freemius.php:18279, includes/class-freemius.php:18392, includes/class-freemius.php:18569 +msgid "Couldn't activate %s." +msgstr "" + +#: includes/class-freemius.php:18280, includes/class-freemius.php:18393, includes/class-freemius.php:18570 +msgid "Please contact us with the following message:" +msgstr "" + +#: includes/class-freemius.php:18389, templates/forms/data-debug-mode.php:162 +msgid "An unknown error has occurred." +msgstr "" + +#: includes/class-freemius.php:18931, includes/class-freemius.php:24339 +msgid "Upgrade" +msgstr "" + +#: includes/class-freemius.php:18937 +msgid "Start Trial" +msgstr "" + +#: includes/class-freemius.php:18939 +msgid "Pricing" +msgstr "" + +#: includes/class-freemius.php:19019, includes/class-freemius.php:19021 +msgid "Affiliation" +msgstr "" + +#: includes/class-freemius.php:19049, includes/class-freemius.php:19051, templates/account.php:264, templates/debug.php:366 +msgid "Account" +msgstr "" + +#: includes/class-freemius.php:19065, includes/class-freemius.php:19067, includes/customizer/class-fs-customizer-support-section.php:60 +msgid "Contact Us" +msgstr "" + +#: includes/class-freemius.php:19078, includes/class-freemius.php:19080, includes/class-freemius.php:24353, templates/account.php:134, templates/account/partials/addon.php:49 +msgid "Add-Ons" +msgstr "" + +#: includes/class-freemius.php:19114 +msgctxt "ASCII arrow left icon" +msgid "←" +msgstr "" + +#: includes/class-freemius.php:19114 +msgctxt "ASCII arrow right icon" +msgid "➤" +msgstr "" + +#: includes/class-freemius.php:19116, templates/pricing.php:110 +msgctxt "noun" +msgid "Pricing" +msgstr "" + +#: includes/class-freemius.php:19329, includes/customizer/class-fs-customizer-support-section.php:67 +msgid "Support Forum" +msgstr "" + +#: includes/class-freemius.php:20350 +msgid "Your email has been successfully verified - you are AWESOME!" +msgstr "" + +#: includes/class-freemius.php:20351 +msgctxt "a positive response" +msgid "Right on" +msgstr "" + +#: includes/class-freemius.php:20857 +msgid "seems like the key you entered doesn't match our records." +msgstr "" + +#: includes/class-freemius.php:20881 +msgid "Debug mode was successfully enabled and will be automatically disabled in 60 min. You can also disable it earlier by clicking the \"Stop Debug\" link." +msgstr "" + +#: includes/class-freemius.php:21116 +msgid "Your %s Add-on plan was successfully upgraded." +msgstr "" + +#: includes/class-freemius.php:21118 +msgid "%s Add-on was successfully purchased." +msgstr "" + +#: includes/class-freemius.php:21121 +msgid "Download the latest version" +msgstr "" + +#: includes/class-freemius.php:21239 +msgid "It seems like one of the authentication parameters is wrong. Update your Public Key, Secret Key & User ID, and try again." +msgstr "" + +#: includes/class-freemius.php:21239, includes/class-freemius.php:21636, includes/class-freemius.php:21737, includes/class-freemius.php:21824 +msgid "Error received from the server:" +msgstr "" + +#: includes/class-freemius.php:21470, includes/class-freemius.php:21742, includes/class-freemius.php:21795, includes/class-freemius.php:21902 +msgctxt "something somebody says when they are thinking about what you have just said." +msgid "Hmm" +msgstr "" + +#: includes/class-freemius.php:21483 +msgid "It looks like you are still on the %s plan. If you did upgrade or change your plan, it's probably an issue on our side - sorry." +msgstr "" + +#: includes/class-freemius.php:21484, templates/account.php:136, templates/add-ons.php:250, templates/account/partials/addon.php:51 +msgctxt "trial period" +msgid "Trial" +msgstr "" + +#: includes/class-freemius.php:21489 +msgid "I have upgraded my account but when I try to Sync the License, the plan remains %s." +msgstr "" + +#: includes/class-freemius.php:21493, includes/class-freemius.php:21545 +msgid "Please contact us here" +msgstr "" + +#: includes/class-freemius.php:21515 +msgid "Your plan was successfully changed to %s." +msgstr "" + +#: includes/class-freemius.php:21531 +msgid "Your license has expired. You can still continue using the free %s forever." +msgstr "" + +#: includes/class-freemius.php:21533 +msgid "Your license has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions." +msgstr "" + +#: includes/class-freemius.php:21541 +msgid "Your license has been cancelled. If you think it's a mistake, please contact support." +msgstr "" + +#: includes/class-freemius.php:21554 +msgid "Your license has expired. You can still continue using all the %s features, but you'll need to renew your license to continue getting updates and support." +msgstr "" + +#: includes/class-freemius.php:21580 +msgid "Your free trial has expired. You can still continue using all our free features." +msgstr "" + +#: includes/class-freemius.php:21582 +msgid "Your free trial has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions." +msgstr "" + +#: includes/class-freemius.php:21628 +msgid "Your server is blocking the access to Freemius' API, which is crucial for %1$s synchronization. Please contact your host to whitelist the following domains:%2$s" +msgstr "" + +#: includes/class-freemius.php:21630 +msgid "Show error details" +msgstr "" + +#: includes/class-freemius.php:21733 +msgid "It looks like the license could not be activated." +msgstr "" + +#: includes/class-freemius.php:21775 +msgid "Your license was successfully activated." +msgstr "" + +#: includes/class-freemius.php:21799 +msgid "It looks like your site currently doesn't have an active license." +msgstr "" + +#: includes/class-freemius.php:21823 +msgid "It looks like the license deactivation failed." +msgstr "" + +#: includes/class-freemius.php:21852 +msgid "Your %s license was successfully deactivated." +msgstr "" + +#: includes/class-freemius.php:21853 +msgid "Your license was successfully deactivated, you are back to the %s plan." +msgstr "" + +#: includes/class-freemius.php:21856 +msgid "O.K" +msgstr "" + +#: includes/class-freemius.php:21909 +msgid "Seems like we are having some temporary issue with your subscription cancellation. Please try again in few minutes." +msgstr "" + +#: includes/class-freemius.php:21918 +msgid "Your subscription was successfully cancelled. Your %s plan license will expire in %s." +msgstr "" + +#: includes/class-freemius.php:21960 +msgid "You are already running the %s in a trial mode." +msgstr "" + +#: includes/class-freemius.php:21971 +msgid "You already utilized a trial before." +msgstr "" + +#: includes/class-freemius.php:21985 +msgid "Plan %s do not exist, therefore, can't start a trial." +msgstr "" + +#: includes/class-freemius.php:21996 +msgid "Plan %s does not support a trial period." +msgstr "" + +#: includes/class-freemius.php:22007 +msgid "None of the %s's plans supports a trial period." +msgstr "" + +#: includes/class-freemius.php:22056 +msgid "It looks like you are not in trial mode anymore so there's nothing to cancel :)" +msgstr "" + +#: includes/class-freemius.php:22092 +msgid "Seems like we are having some temporary issue with your trial cancellation. Please try again in few minutes." +msgstr "" + +#: includes/class-freemius.php:22111 +msgid "Your %s free trial was successfully cancelled." +msgstr "" + +#: includes/class-freemius.php:22438 +msgid "Version %s was released." +msgstr "" + +#: includes/class-freemius.php:22438 +msgid "Please download %s." +msgstr "" + +#: includes/class-freemius.php:22445 +msgid "the latest %s version here" +msgstr "" + +#: includes/class-freemius.php:22450 +msgid "New" +msgstr "" + +#: includes/class-freemius.php:22455 +msgid "Seems like you got the latest release." +msgstr "" + +#: includes/class-freemius.php:22456 +msgid "You are all good!" +msgstr "" + +#: includes/class-freemius.php:22859 +msgid "Verification mail was just sent to %s. If you can't find it after 5 min, please check your spam box." +msgstr "" + +#: includes/class-freemius.php:22999 +msgid "Site successfully opted in." +msgstr "" + +#: includes/class-freemius.php:23000, includes/class-freemius.php:24049 +msgid "Awesome" +msgstr "" + +#: includes/class-freemius.php:23016 +msgid "Sharing diagnostic data with %s helps to provide functionality that's more relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the plugin should be translated and tailored to." +msgstr "" + +#: includes/class-freemius.php:23017 +msgid "Thank you!" +msgstr "" + +#: includes/class-freemius.php:23026 +msgid "Diagnostic data will no longer be sent from %s to %s." +msgstr "" + +#: includes/class-freemius.php:23181 +msgid "A confirmation email was just sent to %s. The email owner must confirm the update within the next 4 hours." +msgstr "" + +#: includes/class-freemius.php:23183 +msgid "A confirmation email was just sent to %s. You must confirm the update within the next 4 hours. If you cannot find the email, please check your spam folder." +msgstr "" + +#: includes/class-freemius.php:23190 +msgid "Thanks for confirming the ownership change. An email was just sent to %s for final approval." +msgstr "" + +#: includes/class-freemius.php:23195 +msgid "%s is the new owner of the account." +msgstr "" + +#: includes/class-freemius.php:23197 +msgctxt "as congratulations" +msgid "Congrats" +msgstr "" + +#: includes/class-freemius.php:23214 +msgid "Please provide your full name." +msgstr "" + +#: includes/class-freemius.php:23219 +msgid "Your name was successfully updated." +msgstr "" + +#: includes/class-freemius.php:23280 +msgid "You have successfully updated your %s." +msgstr "" + +#: includes/class-freemius.php:23339 +msgid "Is this your client's site? %s if you wish to hide sensitive info like your email, license key, prices, billing address & invoices from the WP Admin." +msgstr "" + +#: includes/class-freemius.php:23342 +msgid "Click here" +msgstr "" + +#: includes/class-freemius.php:23379, includes/class-freemius.php:23376 msgid "Bundle" msgstr "" -#: includes/class-fs-plugin-updater.php:206, templates/forms/premium-versions-upgrade-handler.php:57 +#: includes/class-freemius.php:23459 +msgid "Just letting you know that the add-ons information of %s is being pulled from an external server." +msgstr "" + +#: includes/class-freemius.php:23460 +msgctxt "advance notice of something that will need attention." +msgid "Heads up" +msgstr "" + +#: includes/class-freemius.php:24089 +msgctxt "exclamation" +msgid "Hey" +msgstr "" + +#: includes/class-freemius.php:24089 +msgid "How do you like %s so far? Test all our %s premium features with a %d-day free trial." +msgstr "" + +#: includes/class-freemius.php:24097 +msgid "No commitment for %s days - cancel anytime!" +msgstr "" + +#: includes/class-freemius.php:24098 +msgid "No credit card required" +msgstr "" + +#: includes/class-freemius.php:24105, templates/forms/trial-start.php:53 +msgctxt "call to action" +msgid "Start free trial" +msgstr "" + +#: includes/class-freemius.php:24182 +msgid "Hey there, did you know that %s has an affiliate program? If you like the %s you can become our ambassador and earn some cash!" +msgstr "" + +#: includes/class-freemius.php:24191 +msgid "Learn more" +msgstr "" + +#: includes/class-freemius.php:24377, templates/account.php:573, templates/account.php:725, templates/connect.php:221, templates/connect.php:447, includes/managers/class-fs-clone-manager.php:1295, templates/forms/license-activation.php:27, templates/account/partials/addon.php:326 +msgid "Activate License" +msgstr "" + +#: includes/class-freemius.php:24378, templates/account.php:667, templates/account.php:724, templates/account/partials/addon.php:327, templates/account/partials/site.php:273 +msgid "Change License" +msgstr "" + +#: includes/class-freemius.php:24485, templates/account/partials/site.php:170 +msgid "Opt Out" +msgstr "" + +#: includes/class-freemius.php:24487, includes/class-freemius.php:24493, templates/account/partials/site.php:49, templates/account/partials/site.php:170 +msgid "Opt In" +msgstr "" + +#: includes/class-freemius.php:24728 +msgid " The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s" +msgstr "" + +#: includes/class-freemius.php:24738 +msgid "Activate %s features" +msgstr "" + +#: includes/class-freemius.php:24751 +msgid "Please follow these steps to complete the upgrade" +msgstr "" + +#: includes/class-freemius.php:24755 +msgid "Download the latest %s version" +msgstr "" + +#: includes/class-freemius.php:24759 +msgid "Upload and activate the downloaded version" +msgstr "" + +#: includes/class-freemius.php:24761 +msgid "How to upload and activate?" +msgstr "" + +#: includes/class-freemius.php:24796 +msgid "Your plan was successfully upgraded." +msgstr "" + +#: includes/class-freemius.php:24797 +msgid "Your plan was successfully activated." +msgstr "" + +#: includes/class-freemius.php:24927 +msgid "%sClick here%s to choose the sites where you'd like to activate the license on." +msgstr "" + +#: includes/class-freemius.php:25096 +msgid "Auto installation only works for opted-in users." +msgstr "" + +#: includes/class-freemius.php:25106, includes/class-freemius.php:25139, includes/class-fs-plugin-updater.php:1294, includes/class-fs-plugin-updater.php:1308 +msgid "Invalid module ID." +msgstr "" + +#: includes/class-freemius.php:25115, includes/class-fs-plugin-updater.php:1330 +msgid "Premium version already active." +msgstr "" + +#: includes/class-freemius.php:25122 +msgid "You do not have a valid license to access the premium version." +msgstr "" + +#: includes/class-freemius.php:25129 +msgid "Plugin is a \"Serviceware\" which means it does not have a premium code version." +msgstr "" + +#: includes/class-freemius.php:25147, includes/class-fs-plugin-updater.php:1329 +msgid "Premium add-on version already installed." +msgstr "" + +#: includes/class-freemius.php:25501 +msgid "View paid features" +msgstr "" + +#: includes/class-freemius.php:25805 +msgid "Thank you so much for using %s and its add-ons!" +msgstr "" + +#: includes/class-freemius.php:25806 +msgid "Thank you so much for using %s!" +msgstr "" + +#: includes/class-freemius.php:25812 +msgid "You've already opted-in to our usage-tracking, which helps us keep improving the %s." +msgstr "" + +#: includes/class-freemius.php:25816 +msgid "Thank you so much for using our products!" +msgstr "" + +#: includes/class-freemius.php:25817 +msgid "You've already opted-in to our usage-tracking, which helps us keep improving them." +msgstr "" + +#: includes/class-freemius.php:25836 +msgid "%s and its add-ons" +msgstr "" + +#: includes/class-freemius.php:25845 +msgid "Products" +msgstr "" + +#: includes/class-freemius.php:25852, templates/connect.php:322 +msgid "Yes" +msgstr "" + +#: includes/class-freemius.php:25853, templates/connect.php:323 +msgid "send me security & feature updates, educational content and offers." +msgstr "" + +#: includes/class-freemius.php:25854, templates/connect.php:328 +msgid "No" +msgstr "" + +#: includes/class-freemius.php:25856, templates/connect.php:330 +msgid "do %sNOT%s send me security & feature updates, educational content and offers." +msgstr "" + +#: includes/class-freemius.php:25866 +msgid "Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard :-)" +msgstr "" + +#: includes/class-freemius.php:25868, templates/connect.php:337 +msgid "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:" +msgstr "" + +#: includes/class-freemius.php:26158 +msgid "License key is empty." +msgstr "" + +#: includes/class-fs-plugin-updater.php:210, templates/forms/premium-versions-upgrade-handler.php:57 msgid "Renew license" msgstr "" -#: includes/class-fs-plugin-updater.php:211, templates/forms/premium-versions-upgrade-handler.php:58 +#: includes/class-fs-plugin-updater.php:215, templates/forms/premium-versions-upgrade-handler.php:58 msgid "Buy license" msgstr "" -#: includes/class-fs-plugin-updater.php:364, includes/class-fs-plugin-updater.php:331 +#: includes/class-fs-plugin-updater.php:335, includes/class-fs-plugin-updater.php:368 msgid "There is a %s of %s available." msgstr "" -#: includes/class-fs-plugin-updater.php:369, includes/class-fs-plugin-updater.php:333 +#: includes/class-fs-plugin-updater.php:337, includes/class-fs-plugin-updater.php:373 msgid "new Beta version" msgstr "" -#: includes/class-fs-plugin-updater.php:370, includes/class-fs-plugin-updater.php:334 +#: includes/class-fs-plugin-updater.php:338, includes/class-fs-plugin-updater.php:374 msgid "new version" msgstr "" -#: includes/class-fs-plugin-updater.php:393 +#: includes/class-fs-plugin-updater.php:397 msgid "Important Upgrade Notice:" msgstr "" -#: includes/class-fs-plugin-updater.php:1551 +#: includes/class-fs-plugin-updater.php:1359 +msgid "Installing plugin: %s" +msgstr "" + +#: includes/class-fs-plugin-updater.php:1400 +msgid "Unable to connect to the filesystem. Please confirm your credentials." +msgstr "" + +#: includes/class-fs-plugin-updater.php:1582 msgid "The remote plugin package does not contain a folder with the desired slug and renaming did not work." msgstr "" -#: includes/fs-plugin-info-dialog.php:541 +#: includes/fs-plugin-info-dialog.php:542 msgid "Purchase More" msgstr "" -#: includes/fs-plugin-info-dialog.php:542, templates/account/partials/addon.php:390 +#: includes/fs-plugin-info-dialog.php:543, templates/account/partials/addon.php:390 msgctxt "verb" msgid "Purchase" msgstr "" -#. translators: %s: N-days trial -#: includes/fs-plugin-info-dialog.php:546 +#: includes/fs-plugin-info-dialog.php:547 msgid "Start my free %s" msgstr "" -#: includes/fs-plugin-info-dialog.php:754 -msgid "Install Free Version Now" +#: includes/fs-plugin-info-dialog.php:745 +msgid "Install Free Version Update Now" msgstr "" -#: includes/fs-plugin-info-dialog.php:755, templates/add-ons.php:323, templates/auto-installation.php:111, templates/account/partials/addon.php:423, templates/account/partials/addon.php:370 -msgid "Install Now" +#: includes/fs-plugin-info-dialog.php:746, templates/account.php:656 +msgid "Install Update Now" msgstr "" -#: includes/fs-plugin-info-dialog.php:744 -msgid "Install Free Version Update Now" +#: includes/fs-plugin-info-dialog.php:755 +msgid "Install Free Version Now" msgstr "" -#: includes/fs-plugin-info-dialog.php:745, templates/account.php:650 -msgid "Install Update Now" +#: includes/fs-plugin-info-dialog.php:756, templates/add-ons.php:323, templates/auto-installation.php:111, templates/account/partials/addon.php:370, templates/account/partials/addon.php:423 +msgid "Install Now" msgstr "" -#: includes/fs-plugin-info-dialog.php:771 +#: includes/fs-plugin-info-dialog.php:772 msgctxt "as download latest version" msgid "Download Latest Free Version" msgstr "" -#: includes/fs-plugin-info-dialog.php:772, templates/account.php:109, templates/add-ons.php:37, templates/account/partials/addon.php:30 +#: includes/fs-plugin-info-dialog.php:773, templates/account.php:114, templates/add-ons.php:37, templates/account/partials/addon.php:30 msgctxt "as download latest version" msgid "Download Latest" msgstr "" -#: includes/fs-plugin-info-dialog.php:787, templates/add-ons.php:329, templates/account/partials/addon.php:417, templates/account/partials/addon.php:361 +#: includes/fs-plugin-info-dialog.php:788, templates/add-ons.php:329, templates/account/partials/addon.php:361, templates/account/partials/addon.php:417 msgid "Activate this add-on" msgstr "" -#: includes/fs-plugin-info-dialog.php:789, templates/connect.php:483 +#: includes/fs-plugin-info-dialog.php:790, templates/connect.php:444 msgid "Activate Free Version" msgstr "" -#: includes/fs-plugin-info-dialog.php:790, templates/account.php:133, templates/add-ons.php:330, templates/account/partials/addon.php:53 +#: includes/fs-plugin-info-dialog.php:791, templates/account.php:138, templates/add-ons.php:330, templates/account/partials/addon.php:53 msgid "Activate" msgstr "" -#: includes/fs-plugin-info-dialog.php:1002 +#: includes/fs-plugin-info-dialog.php:1003 msgctxt "Plugin installer section title" msgid "Description" msgstr "" -#: includes/fs-plugin-info-dialog.php:1003 +#: includes/fs-plugin-info-dialog.php:1004 msgctxt "Plugin installer section title" msgid "Installation" msgstr "" -#: includes/fs-plugin-info-dialog.php:1004 +#: includes/fs-plugin-info-dialog.php:1005 msgctxt "Plugin installer section title" msgid "FAQ" msgstr "" -#: includes/fs-plugin-info-dialog.php:1005, templates/plugin-info/description.php:55 +#: includes/fs-plugin-info-dialog.php:1006, templates/plugin-info/description.php:55 msgid "Screenshots" msgstr "" -#: includes/fs-plugin-info-dialog.php:1006 +#: includes/fs-plugin-info-dialog.php:1007 msgctxt "Plugin installer section title" msgid "Changelog" msgstr "" -#: includes/fs-plugin-info-dialog.php:1007 +#: includes/fs-plugin-info-dialog.php:1008 msgctxt "Plugin installer section title" msgid "Reviews" msgstr "" -#: includes/fs-plugin-info-dialog.php:1008 +#: includes/fs-plugin-info-dialog.php:1009 msgctxt "Plugin installer section title" msgid "Other Notes" msgstr "" -#: includes/fs-plugin-info-dialog.php:1023 +#: includes/fs-plugin-info-dialog.php:1024 msgctxt "Plugin installer section title" msgid "Features & Pricing" msgstr "" -#: includes/fs-plugin-info-dialog.php:1033 +#: includes/fs-plugin-info-dialog.php:1034 msgid "Plugin Install" msgstr "" -#: includes/fs-plugin-info-dialog.php:1105 +#: includes/fs-plugin-info-dialog.php:1106 msgctxt "e.g. Professional Plan" msgid "%s Plan" msgstr "" -#: includes/fs-plugin-info-dialog.php:1131 +#: includes/fs-plugin-info-dialog.php:1132 msgctxt "e.g. the best product" msgid "Best" msgstr "" -#: includes/fs-plugin-info-dialog.php:1137, includes/fs-plugin-info-dialog.php:1157 +#: includes/fs-plugin-info-dialog.php:1138, includes/fs-plugin-info-dialog.php:1158 msgctxt "as every month" msgid "Monthly" msgstr "" -#: includes/fs-plugin-info-dialog.php:1140 +#: includes/fs-plugin-info-dialog.php:1141 msgctxt "as once a year" msgid "Annual" msgstr "" -#: includes/fs-plugin-info-dialog.php:1143 +#: includes/fs-plugin-info-dialog.php:1144 msgid "Lifetime" msgstr "" -#: includes/fs-plugin-info-dialog.php:1157, includes/fs-plugin-info-dialog.php:1159, includes/fs-plugin-info-dialog.php:1161 +#: includes/fs-plugin-info-dialog.php:1158, includes/fs-plugin-info-dialog.php:1160, includes/fs-plugin-info-dialog.php:1162 msgctxt "e.g. billed monthly" msgid "Billed %s" msgstr "" -#: includes/fs-plugin-info-dialog.php:1159 +#: includes/fs-plugin-info-dialog.php:1160 msgctxt "as once a year" msgid "Annually" msgstr "" -#: includes/fs-plugin-info-dialog.php:1161 +#: includes/fs-plugin-info-dialog.php:1162 msgctxt "as once a year" msgid "Once" msgstr "" -#: includes/fs-plugin-info-dialog.php:1167 +#: includes/fs-plugin-info-dialog.php:1168 msgid "Single Site License" msgstr "" -#: includes/fs-plugin-info-dialog.php:1169 +#: includes/fs-plugin-info-dialog.php:1170 msgid "Unlimited Licenses" msgstr "" -#: includes/fs-plugin-info-dialog.php:1171 +#: includes/fs-plugin-info-dialog.php:1172 msgid "Up to %s Sites" msgstr "" -#: includes/fs-plugin-info-dialog.php:1181, templates/plugin-info/features.php:82 +#: includes/fs-plugin-info-dialog.php:1182, templates/plugin-info/features.php:82 msgctxt "as monthly period" msgid "mo" msgstr "" -#: includes/fs-plugin-info-dialog.php:1188, templates/plugin-info/features.php:80 +#: includes/fs-plugin-info-dialog.php:1189, templates/plugin-info/features.php:80 msgctxt "as annual period" msgid "year" msgstr "" -#: includes/fs-plugin-info-dialog.php:1242 +#: includes/fs-plugin-info-dialog.php:1243 msgctxt "noun" msgid "Price" msgstr "" -#. translators: %s: Discount (e.g. discount of $5 or 10%) -#: includes/fs-plugin-info-dialog.php:1290 +#: includes/fs-plugin-info-dialog.php:1291 msgid "Save %s" msgstr "" -#: includes/fs-plugin-info-dialog.php:1300 +#: includes/fs-plugin-info-dialog.php:1301 msgid "No commitment for %s - cancel anytime" msgstr "" -#: includes/fs-plugin-info-dialog.php:1303 +#: includes/fs-plugin-info-dialog.php:1304 msgid "After your free %s, pay as little as %s" msgstr "" -#: includes/fs-plugin-info-dialog.php:1314 +#: includes/fs-plugin-info-dialog.php:1315 msgid "Details" msgstr "" -#: includes/fs-plugin-info-dialog.php:1318, templates/account.php:120, templates/debug.php:215, templates/debug.php:252, templates/debug.php:466, templates/account/partials/addon.php:41 +#: includes/fs-plugin-info-dialog.php:1319, templates/account.php:125, templates/debug.php:232, templates/debug.php:269, templates/debug.php:518, templates/account/partials/addon.php:41 msgctxt "product version" msgid "Version" msgstr "" -#: includes/fs-plugin-info-dialog.php:1325 +#: includes/fs-plugin-info-dialog.php:1326 msgctxt "as the plugin author" msgid "Author" msgstr "" -#: includes/fs-plugin-info-dialog.php:1332 +#: includes/fs-plugin-info-dialog.php:1333 msgid "Last Updated" msgstr "" -#. translators: %s: time period (e.g. "2 hours" ago) -#: includes/fs-plugin-info-dialog.php:1337, templates/account.php:536 +#: includes/fs-plugin-info-dialog.php:1338, templates/account.php:544 msgctxt "x-ago" msgid "%s ago" msgstr "" -#: includes/fs-plugin-info-dialog.php:1346 +#: includes/fs-plugin-info-dialog.php:1347 msgid "Requires WordPress Version" msgstr "" -#: includes/fs-plugin-info-dialog.php:1347 +#: includes/fs-plugin-info-dialog.php:1350, includes/fs-plugin-info-dialog.php:1370 msgid "%s or higher" msgstr "" -#: includes/fs-plugin-info-dialog.php:1354 +#: includes/fs-plugin-info-dialog.php:1358 msgid "Compatible up to" msgstr "" -#: includes/fs-plugin-info-dialog.php:1362 +#: includes/fs-plugin-info-dialog.php:1366 +msgid "Requires PHP Version" +msgstr "" + +#: includes/fs-plugin-info-dialog.php:1379 msgid "Downloaded" msgstr "" -#. translators: %s: 1 or One (Number of times downloaded) -#: includes/fs-plugin-info-dialog.php:1366 +#: includes/fs-plugin-info-dialog.php:1383 msgid "%s time" msgstr "" -#. translators: %s: Number of times downloaded -#: includes/fs-plugin-info-dialog.php:1368 +#: includes/fs-plugin-info-dialog.php:1385 msgid "%s times" msgstr "" -#: includes/fs-plugin-info-dialog.php:1379 +#: includes/fs-plugin-info-dialog.php:1396 msgid "WordPress.org Plugin Page" msgstr "" -#: includes/fs-plugin-info-dialog.php:1388 +#: includes/fs-plugin-info-dialog.php:1405 msgid "Plugin Homepage" msgstr "" -#: includes/fs-plugin-info-dialog.php:1397, includes/fs-plugin-info-dialog.php:1481 +#: includes/fs-plugin-info-dialog.php:1414, includes/fs-plugin-info-dialog.php:1498 msgid "Donate to this plugin" msgstr "" -#: includes/fs-plugin-info-dialog.php:1404 +#: includes/fs-plugin-info-dialog.php:1421 msgid "Average Rating" msgstr "" -#: includes/fs-plugin-info-dialog.php:1411 +#: includes/fs-plugin-info-dialog.php:1428 msgid "based on %s" msgstr "" -#. translators: %s: 1 or One -#: includes/fs-plugin-info-dialog.php:1415 +#: includes/fs-plugin-info-dialog.php:1432 msgid "%s rating" msgstr "" -#. translators: %s: Number larger than 1 -#: includes/fs-plugin-info-dialog.php:1417 +#: includes/fs-plugin-info-dialog.php:1434 msgid "%s ratings" msgstr "" -#. translators: %s: 1 or One -#: includes/fs-plugin-info-dialog.php:1432 +#: includes/fs-plugin-info-dialog.php:1449 msgid "%s star" msgstr "" -#. translators: %s: Number larger than 1 -#: includes/fs-plugin-info-dialog.php:1434 +#: includes/fs-plugin-info-dialog.php:1451 msgid "%s stars" msgstr "" -#. translators: %s: # of stars (e.g. 5 stars) -#: includes/fs-plugin-info-dialog.php:1446 +#: includes/fs-plugin-info-dialog.php:1463 msgid "Click to see reviews that provided a rating of %s" msgstr "" -#: includes/fs-plugin-info-dialog.php:1459 +#: includes/fs-plugin-info-dialog.php:1476 msgid "Contributors" msgstr "" -#: includes/fs-plugin-info-dialog.php:1491, includes/fs-plugin-info-dialog.php:1489 -msgid "Warning" +#: includes/fs-plugin-info-dialog.php:1517 +msgid "This plugin requires a newer version of PHP." msgstr "" -#: includes/fs-plugin-info-dialog.php:1491 -msgid "This plugin has not been marked as compatible with your version of WordPress." +#: includes/fs-plugin-info-dialog.php:1526 +msgid "Click here to learn more about updating PHP." msgstr "" -#: includes/fs-plugin-info-dialog.php:1489 +#: includes/fs-plugin-info-dialog.php:1540, includes/fs-plugin-info-dialog.php:1542 +msgid "Warning" +msgstr "" + +#: includes/fs-plugin-info-dialog.php:1540 msgid "This plugin has not been tested with your current version of WordPress." msgstr "" -#: includes/fs-plugin-info-dialog.php:1510 +#: includes/fs-plugin-info-dialog.php:1542 +msgid "This plugin has not been marked as compatible with your version of WordPress." +msgstr "" + +#: includes/fs-plugin-info-dialog.php:1561 msgid "Paid add-on must be deployed to Freemius." msgstr "" -#: includes/fs-plugin-info-dialog.php:1511 +#: includes/fs-plugin-info-dialog.php:1562 msgid "Add-on must be deployed to WordPress.org or Freemius." msgstr "" -#: includes/fs-plugin-info-dialog.php:1540 -msgid "Latest Version Installed" +#: includes/fs-plugin-info-dialog.php:1583 +msgid "Newer Version (%s) Installed" msgstr "" -#: includes/fs-plugin-info-dialog.php:1541 -msgid "Latest Free Version Installed" +#: includes/fs-plugin-info-dialog.php:1584 +msgid "Newer Free Version (%s) Installed" msgstr "" -#: includes/fs-plugin-info-dialog.php:1532 -msgid "Newer Version (%s) Installed" +#: includes/fs-plugin-info-dialog.php:1591 +msgid "Latest Version Installed" msgstr "" -#: includes/fs-plugin-info-dialog.php:1533 -msgid "Newer Free Version (%s) Installed" +#: includes/fs-plugin-info-dialog.php:1592 +msgid "Latest Free Version Installed" msgstr "" -#: templates/account.php:110, templates/forms/subscription-cancellation.php:96, templates/account/partials/addon.php:31, templates/account/partials/site.php:311 +#: templates/account.php:115, templates/forms/subscription-cancellation.php:96, templates/account/partials/addon.php:31, templates/account/partials/site.php:313 msgid "Downgrading your plan" msgstr "" -#: templates/account.php:111, templates/forms/subscription-cancellation.php:97, templates/account/partials/addon.php:32, templates/account/partials/site.php:312 +#: templates/account.php:116, templates/forms/subscription-cancellation.php:97, templates/account/partials/addon.php:32, templates/account/partials/site.php:314 msgid "Cancelling the subscription" msgstr "" #. translators: %1$s: Either 'Downgrading your plan' or 'Cancelling the subscription' -#: templates/account.php:113, templates/forms/subscription-cancellation.php:99, templates/account/partials/site.php:314 +#: templates/account.php:118, templates/forms/subscription-cancellation.php:99, templates/account/partials/site.php:316 msgid "%1$s will immediately stop all future recurring payments and your %2$s plan license will expire in %3$s." msgstr "" -#: templates/account.php:114, templates/forms/subscription-cancellation.php:100, templates/account/partials/addon.php:35, templates/account/partials/site.php:315 +#: templates/account.php:119, templates/forms/subscription-cancellation.php:100, templates/account/partials/addon.php:35, templates/account/partials/site.php:317 msgid "Please note that we will not be able to grandfather outdated pricing for renewals/new subscriptions after a cancellation. If you choose to renew the subscription manually in the future, after a price increase, which typically occurs once a year, you will be charged the updated price." msgstr "" -#: templates/account.php:115, templates/forms/subscription-cancellation.php:106, templates/account/partials/addon.php:36 +#: templates/account.php:120, templates/forms/subscription-cancellation.php:106, templates/account/partials/addon.php:36 msgid "Cancelling the trial will immediately block access to all premium features. Are you sure?" msgstr "" -#: templates/account.php:116, templates/forms/subscription-cancellation.php:101, templates/account/partials/addon.php:37, templates/account/partials/site.php:316 +#: templates/account.php:121, templates/forms/subscription-cancellation.php:101, templates/account/partials/addon.php:37, templates/account/partials/site.php:318 msgid "You can still enjoy all %s features but you will not have access to %s security & feature updates, nor support." msgstr "" -#: templates/account.php:117, templates/forms/subscription-cancellation.php:102, templates/account/partials/addon.php:38, templates/account/partials/site.php:317 +#: templates/account.php:122, templates/forms/subscription-cancellation.php:102, templates/account/partials/addon.php:38, templates/account/partials/site.php:319 msgid "Once your license expires you can still use the Free version but you will NOT have access to the %s features." msgstr "" #. translators: %s: Plan title (e.g. "Professional") -#: templates/account.php:119, templates/account/partials/activate-license-button.php:31, templates/account/partials/addon.php:40 +#: templates/account.php:124, templates/account/partials/activate-license-button.php:31, templates/account/partials/addon.php:40 msgid "Activate %s Plan" msgstr "" #. translators: %s: Time period (e.g. Auto renews in "2 months") -#: templates/account.php:122, templates/account/partials/addon.php:43, templates/account/partials/site.php:291 +#: templates/account.php:127, templates/account/partials/addon.php:43, templates/account/partials/site.php:293 msgid "Auto renews in %s" msgstr "" #. translators: %s: Time period (e.g. Expires in "2 months") -#: templates/account.php:124, templates/account/partials/addon.php:45, templates/account/partials/site.php:293 +#: templates/account.php:129, templates/account/partials/addon.php:45, templates/account/partials/site.php:295 msgid "Expires in %s" msgstr "" -#: templates/account.php:125 +#: templates/account.php:130 msgctxt "as synchronize license" msgid "Sync License" msgstr "" -#: templates/account.php:126, templates/account/partials/addon.php:46 +#: templates/account.php:131, templates/account/partials/addon.php:46 msgid "Cancel Trial" msgstr "" -#: templates/account.php:127, templates/account/partials/addon.php:47 +#: templates/account.php:132, templates/account/partials/addon.php:47 msgid "Change Plan" msgstr "" -#: templates/account.php:128, templates/account/partials/addon.php:48 +#: templates/account.php:133, templates/account/partials/addon.php:48 msgctxt "verb" msgid "Upgrade" msgstr "" -#: templates/account.php:129, templates/account/partials/addon.php:49 -msgid "Add-Ons" -msgstr "" - -#: templates/account.php:130, templates/account/partials/addon.php:50, templates/account/partials/site.php:318 +#: templates/account.php:135, templates/account/partials/addon.php:50, templates/account/partials/site.php:320 msgctxt "verb" msgid "Downgrade" msgstr "" -#: templates/account.php:131, templates/add-ons.php:250, templates/account/partials/addon.php:51 -msgctxt "trial period" -msgid "Trial" -msgstr "" - -#: templates/account.php:132, templates/add-ons.php:246, templates/plugin-info/features.php:72, templates/account/partials/addon.php:52, templates/account/partials/site.php:33 +#: templates/account.php:137, templates/add-ons.php:246, templates/plugin-info/features.php:72, templates/account/partials/addon.php:52, templates/account/partials/site.php:33 msgid "Free" msgstr "" -#: templates/account.php:134, templates/debug.php:385, templates/account/partials/addon.php:54 +#: templates/account.php:139, templates/debug.php:412, includes/customizer/class-fs-customizer-upsell-control.php:110, templates/account/partials/addon.php:54 msgctxt "as product pricing plan" msgid "Plan" msgstr "" -#: templates/account.php:135 +#: templates/account.php:140 msgid "Bundle Plan" msgstr "" -#: templates/account.php:251, templates/debug.php:338 -msgid "Account" -msgstr "" - -#: templates/account.php:259 +#: templates/account.php:272 msgid "Free Trial" msgstr "" -#: templates/account.php:270 +#: templates/account.php:283 msgid "Account Details" msgstr "" -#: templates/account.php:279 -msgid "Stop Debug" -msgstr "" - -#: templates/account.php:277, templates/forms/data-debug-mode.php:33 +#: templates/account.php:290, templates/forms/data-debug-mode.php:33 msgid "Start Debug" msgstr "" -#: templates/account.php:286 -msgid "Billing & Invoices" +#: templates/account.php:292 +msgid "Stop Debug" msgstr "" #: templates/account.php:299 -msgid "Deletion is not temporary. Only delete if you no longer want to use this %s anymore. Are you sure you would like to continue with the deletion?" -msgstr "" - -#: templates/account.php:297 -msgid "Deleting the account will automatically deactivate your %s plan license so you can use it on other sites. If you want to terminate the recurring payments as well, click the \"Cancel\" button, and first \"Downgrade\" your account. Are you sure you would like to continue with the deletion?" -msgstr "" - -#: templates/account.php:302 -msgid "Delete Account" +msgid "Billing & Invoices" msgstr "" -#: templates/account.php:314, templates/account/partials/addon.php:236, templates/account/partials/deactivate-license-button.php:35 +#: templates/account.php:322, templates/account/partials/addon.php:236, templates/account/partials/deactivate-license-button.php:35 msgid "Deactivate License" msgstr "" -#: templates/account.php:337, templates/forms/subscription-cancellation.php:125 +#: templates/account.php:345, templates/forms/subscription-cancellation.php:125 msgid "Are you sure you want to proceed?" msgstr "" -#: templates/account.php:337, templates/account/partials/addon.php:260 +#: templates/account.php:345, templates/account/partials/addon.php:260 msgid "Cancel Subscription" msgstr "" -#: templates/account.php:366, templates/account/partials/addon.php:345 +#: templates/account.php:374, templates/account/partials/addon.php:345 msgctxt "as synchronize" msgid "Sync" msgstr "" -#: templates/account.php:381, templates/debug.php:523 +#: templates/account.php:389, templates/debug.php:575 msgid "Name" msgstr "" -#: templates/account.php:387, templates/debug.php:524 +#: templates/account.php:395, templates/debug.php:576 msgid "Email" msgstr "" -#: templates/account.php:394, templates/debug.php:383, templates/debug.php:573 +#: templates/account.php:402, templates/debug.php:410, templates/debug.php:625 msgid "User ID" msgstr "" -#: templates/account.php:403, templates/account.php:411, templates/debug.php:372, templates/debug.php:567 -msgid "Plugin" -msgstr "" - -#: templates/account.php:404, templates/account.php:412, templates/debug.php:372, templates/debug.php:567, templates/forms/deactivation/form.php:107 -msgid "Theme" -msgstr "" - -#: templates/account.php:412, templates/account.php:732, templates/account.php:783, templates/debug.php:250, templates/debug.php:377, templates/debug.php:463, templates/debug.php:522, templates/debug.php:571, templates/debug.php:650, templates/account/payments.php:35, templates/debug/logger.php:21 +#: templates/account.php:420, templates/account.php:738, templates/account.php:789, templates/debug.php:267, templates/debug.php:404, templates/debug.php:515, templates/debug.php:574, templates/debug.php:623, templates/debug.php:702, templates/account/payments.php:35, templates/debug/logger.php:21 msgid "ID" msgstr "" -#: templates/account.php:419 +#: templates/account.php:427 msgid "Site ID" msgstr "" -#: templates/account.php:422 +#: templates/account.php:430 msgid "No ID" msgstr "" -#: templates/account.php:427, templates/debug.php:257, templates/debug.php:386, templates/debug.php:467, templates/debug.php:526, templates/account/partials/site.php:227 +#: templates/account.php:435, templates/debug.php:274, templates/debug.php:413, templates/debug.php:519, templates/debug.php:578, templates/account/partials/site.php:228 msgid "Public Key" msgstr "" -#: templates/account.php:433, templates/debug.php:387, templates/debug.php:468, templates/debug.php:527, templates/account/partials/site.php:239 +#: templates/account.php:441, templates/debug.php:414, templates/debug.php:520, templates/debug.php:579, templates/account/partials/site.php:241 msgid "Secret Key" msgstr "" -#: templates/account.php:436 +#: templates/account.php:444 msgctxt "as secret encryption key missing" msgid "No Secret" msgstr "" -#: templates/account.php:490, templates/debug.php:579, templates/account/partials/site.php:260 -msgid "License Key" +#: templates/account.php:471, templates/account/partials/site.php:120, templates/account/partials/site.php:122 +msgid "Trial" msgstr "" -#: templates/account.php:463, templates/account/partials/site.php:122, templates/account/partials/site.php:120 -msgid "Trial" +#: templates/account.php:498, templates/debug.php:631, templates/account/partials/site.php:262 +msgid "License Key" msgstr "" -#: templates/account.php:521 +#: templates/account.php:529 msgid "Join the Beta program" msgstr "" -#: templates/account.php:527 +#: templates/account.php:535 msgid "not verified" msgstr "" -#: templates/account.php:598 -msgid "Free version" +#: templates/account.php:544, templates/account/partials/addon.php:195 +msgid "Expired" msgstr "" -#: templates/account.php:596 +#: templates/account.php:602 msgid "Premium version" msgstr "" -#: templates/account.php:536, templates/account/partials/addon.php:195 -msgid "Expired" -msgstr "" - -#: templates/account.php:567, templates/account.php:719, templates/connect.php:198, templates/connect.php:486, includes/managers/class-fs-clone-manager.php:1123, templates/forms/license-activation.php:27, templates/account/partials/addon.php:326 -msgid "Activate License" +#: templates/account.php:604 +msgid "Free version" msgstr "" -#: templates/account.php:610 +#: templates/account.php:616 msgid "Verify Email" msgstr "" -#: templates/account.php:687, templates/forms/user-change.php:27 -msgid "Change User" -msgstr "" - -#: templates/account.php:674 -msgid "What is your %s?" +#: templates/account.php:630 +msgid "Download %s Version" msgstr "" -#: templates/account.php:682, templates/account/billing.php:21 -msgctxt "verb" -msgid "Edit" +#: templates/account.php:646 +msgid "Download Paid Version" msgstr "" -#: templates/account.php:658, templates/account.php:921, templates/account/partials/site.php:248, templates/account/partials/site.php:270 +#: templates/account.php:664, templates/account.php:927, templates/account/partials/site.php:250, templates/account/partials/site.php:272 msgctxt "verb" msgid "Show" msgstr "" -#: templates/account.php:661, templates/account.php:718, templates/account/partials/addon.php:327, templates/account/partials/site.php:271 -msgid "Change License" +#: templates/account.php:680 +msgid "What is your %s?" msgstr "" -#: templates/account.php:624 -msgid "Download %s Version" +#: templates/account.php:688, templates/account/billing.php:21 +msgctxt "verb" +msgid "Edit" msgstr "" -#: templates/account.php:640 -msgid "Download Paid Version" +#: templates/account.php:693, templates/forms/user-change.php:27 +msgid "Change User" msgstr "" -#: templates/account.php:711 +#: templates/account.php:717 msgid "Sites" msgstr "" -#: templates/account.php:724 +#: templates/account.php:730 msgid "Search by address" msgstr "" -#: templates/account.php:733, templates/debug.php:380 +#: templates/account.php:739, templates/debug.php:407 msgid "Address" msgstr "" -#: templates/account.php:734 +#: templates/account.php:740 msgid "License" msgstr "" -#: templates/account.php:735 +#: templates/account.php:741 msgid "Plan" msgstr "" -#: templates/account.php:786 +#: templates/account.php:792 msgctxt "as software license" msgid "License" msgstr "" -#: templates/account.php:915 +#: templates/account.php:921 msgctxt "verb" msgid "Hide" msgstr "" -#: templates/account.php:937, templates/forms/data-debug-mode.php:31, templates/forms/deactivation/form.php:358, templates/forms/deactivation/form.php:389 +#: templates/account.php:943, templates/forms/data-debug-mode.php:31, templates/forms/deactivation/form.php:358, templates/forms/deactivation/form.php:389 msgid "Processing" msgstr "" -#: templates/account.php:940 +#: templates/account.php:946 msgid "Get updates for bleeding edge Beta versions of %s." msgstr "" -#: templates/account.php:998 +#: templates/account.php:1004 msgid "Cancelling %s" msgstr "" -#: templates/account.php:998, templates/account.php:1015, templates/forms/subscription-cancellation.php:27, templates/forms/deactivation/form.php:178 +#: templates/account.php:1004, templates/account.php:1021, templates/forms/subscription-cancellation.php:27, templates/forms/deactivation/form.php:178 msgid "trial" msgstr "" -#: templates/account.php:1013, templates/forms/deactivation/form.php:195 +#: templates/account.php:1019, templates/forms/deactivation/form.php:195 msgid "Cancelling %s..." msgstr "" -#: templates/account.php:1016, templates/forms/subscription-cancellation.php:28, templates/forms/deactivation/form.php:179 +#: templates/account.php:1022, templates/forms/subscription-cancellation.php:28, templates/forms/deactivation/form.php:179 msgid "subscription" msgstr "" -#: templates/account.php:1030 +#: templates/account.php:1036 msgid "Deactivating your license will block all premium features, but will enable activating the license on another site. Are you sure you want to proceed?" msgstr "" -#: templates/account.php:1104 +#: templates/account.php:1110 msgid "Disabling white-label mode" msgstr "" -#: templates/account.php:1105 +#: templates/account.php:1111 msgid "Enabling white-label mode" msgstr "" @@ -709,19 +1475,10 @@ msgstr "" msgid "Add Ons for %s" msgstr "" -#: templates/add-ons.php:57 -msgctxt "exclamation" -msgid "Oops" -msgstr "" - #: templates/add-ons.php:58 msgid "We couldn't load the add-ons list. It's probably an issue on our side, please try to come back in few minutes." msgstr "" -#: templates/add-ons.php:186, templates/account/partials/addon.php:386 -msgid "More information about %s" -msgstr "" - #: templates/add-ons.php:229 msgctxt "active add-on" msgid "Active" @@ -732,16 +1489,11 @@ msgctxt "installed add-on" msgid "Installed" msgstr "" -#: templates/admin-notice.php:13, templates/forms/license-activation.php:250, templates/forms/resend-key.php:80 +#: templates/admin-notice.php:13, templates/forms/license-activation.php:243, templates/forms/resend-key.php:80 msgctxt "as close a window" msgid "Dismiss" msgstr "" -#: templates/auto-installation.php:32 -msgid "Add-On" -msgstr "" - -#. translators: %s: Number of seconds #: templates/auto-installation.php:45 msgid "%s sec" msgstr "" @@ -762,147 +1514,160 @@ msgstr "" msgid "Cancel Installation" msgstr "" +#: templates/checkout.php:181 +msgid "Checkout" +msgstr "" + +#: templates/checkout.php:181 +msgid "PCI compliant" +msgstr "" + #. translators: %s: name (e.g. Hey John,) -#: templates/connect.php:121 +#: templates/connect.php:127 msgctxt "greeting" msgid "Hey %s," msgstr "" -#: templates/connect.php:181 -msgid "Allow & Continue" +#: templates/connect.php:187 +msgid "Never miss an important update" msgstr "" -#: templates/connect.php:210, templates/connect.php:217 -msgid "Never miss an important update - opt in to our security & feature updates notifications, educational content, offers, and non-sensitive diagnostic tracking with %4$s." +#: templates/connect.php:195 +msgid "Thank you for updating to %1$s v%2$s!" msgstr "" -#: templates/connect.php:211, templates/connect.php:218 -msgid "Never miss an important update - opt in to our security & feature updates notifications, and non-sensitive diagnostic tracking with %4$s." +#: templates/connect.php:205 +msgid "Allow & Continue" msgstr "" -#: templates/connect.php:221 -msgid "If you skip this, that's okay! %1$s will still work just fine." +#: templates/connect.php:209 +msgid "Re-send activation email" msgstr "" -#: templates/connect.php:199, templates/forms/license-activation.php:46 -msgid "Agree & Activate License" +#: templates/connect.php:213 +msgid "Thanks %s!" msgstr "" -#: templates/connect.php:203 -msgid "Welcome to %s! To get started, please enter your license key:" +#: templates/connect.php:214 +msgid "You should receive a confirmation email for %s to your mailbox at %s. Please make sure you click the button in that email to %s." msgstr "" -#: templates/connect.php:185 -msgid "Re-send activation email" +#: templates/connect.php:225 +msgid "Welcome to %s! To get started, please enter your license key:" msgstr "" -#: templates/connect.php:189 -msgid "Thanks %s!" +#: templates/connect.php:236 +msgid "Opt in to get email notifications for security & feature updates, educational content, and occasional offers, and to share some basic WordPress environment info. This will help us make the %s more compatible with your site and better at doing what you need it to." msgstr "" -#: templates/connect.php:190 -msgid "You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s." +#. translators: %s: module type (plugin, theme, or add-on) +#: templates/connect.php:245 +msgid "We have introduced this opt-in so you never miss an important update and help us make the %s more compatible with your site and better at doing what you need it to." msgstr "" -#: templates/connect.php:194 -msgid "complete the install" +#: templates/connect.php:247 +msgid "Opt in to get email notifications for security & feature updates, educational content, and occasional offers, and to share some basic WordPress environment info." msgstr "" -#: templates/connect.php:251 -msgid "We're excited to introduce the Freemius network-level integration." +#: templates/connect.php:250 +msgid "If you skip this, that's okay! %1$s will still work just fine." msgstr "" -#: templates/connect.php:265 -msgid "During the update process we detected %s site(s) in the network that are still pending your attention." +#: templates/connect.php:280 +msgid "We're excited to introduce the Freemius network-level integration." msgstr "" -#: templates/connect.php:254 +#: templates/connect.php:283 msgid "During the update process we detected %d site(s) that are still pending license activation." msgstr "" -#: templates/connect.php:256 +#: templates/connect.php:285 msgid "If you'd like to use the %s on those sites, please enter your license key below and click the activation button." msgstr "" -#: templates/connect.php:258 +#: templates/connect.php:287 msgid "%s's paid features" msgstr "" -#: templates/connect.php:263 +#: templates/connect.php:292 msgid "Alternatively, you can skip it for now and activate the license later, in your %s's network-level Account page." msgstr "" -#: templates/connect.php:274, templates/forms/data-debug-mode.php:35, templates/forms/license-activation.php:49 -msgid "License key" +#: templates/connect.php:294 +msgid "During the update process we detected %s site(s) in the network that are still pending your attention." msgstr "" -#: templates/connect.php:277, templates/forms/license-activation.php:22 -msgid "Can't find your license key?" +#: templates/connect.php:303, templates/forms/data-debug-mode.php:35, templates/forms/license-activation.php:42 +msgid "License key" msgstr "" -#: templates/connect.php:308 -msgid "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:" +#: templates/connect.php:306, templates/forms/license-activation.php:22 +msgid "Can't find your license key?" msgstr "" -#: templates/connect.php:340, templates/connect.php:730, templates/forms/deactivation/retry-skip.php:20 +#: templates/connect.php:369, templates/connect.php:693, templates/forms/deactivation/retry-skip.php:20 msgctxt "verb" msgid "Skip" msgstr "" -#: templates/connect.php:343 +#: templates/connect.php:372 msgid "Delegate to Site Admins" msgstr "" -#: templates/connect.php:343 +#: templates/connect.php:372 msgid "If you click it, this decision will be delegated to the sites administrators." msgstr "" -#: templates/connect.php:368 +#: templates/connect.php:399 msgid "License issues?" msgstr "" -#: templates/connect.php:454 -msgid "What permissions are being granted?" +#: templates/connect.php:423 +msgid "For delivery of security & feature updates, and license management, %s needs to" msgstr "" -#: templates/connect.php:448 -msgid "The %1$s will periodically send %2$s to %3$s for security & feature updates delivery, and license management." +#: templates/connect.php:428 +msgid "This will allow %s to" msgstr "" -#: templates/connect.php:450 -msgid "diagnostic data" +#: templates/connect.php:443 +msgid "Don't have a license key?" msgstr "" -#: templates/connect.php:485 +#: templates/connect.php:446 msgid "Have a license key?" msgstr "" -#: templates/connect.php:482 -msgid "Don't have a license key?" +#: templates/connect.php:454 +msgid "Freemius is our licensing and software updates engine" msgstr "" -#: templates/connect.php:493 +#: templates/connect.php:457 msgid "Privacy Policy" msgstr "" -#: templates/connect.php:495 +#: templates/connect.php:459 msgid "License Agreement" msgstr "" -#: templates/connect.php:495 +#: templates/connect.php:459 msgid "Terms of Service" msgstr "" -#: templates/connect.php:896 +#: templates/connect.php:879 msgctxt "as in the process of sending an email" msgid "Sending email" msgstr "" -#: templates/connect.php:897 +#: templates/connect.php:880 msgctxt "as activating plugin" msgid "Activating" msgstr "" +#: templates/contact.php:78 +msgid "Contact" +msgstr "" + #: templates/debug.php:17 msgctxt "as turned off" msgid "Off" @@ -913,243 +1678,255 @@ msgctxt "as turned on" msgid "On" msgstr "" -#: templates/debug.php:20 +#: templates/debug.php:24 msgid "SDK" msgstr "" -#: templates/debug.php:24 +#: templates/debug.php:28 msgctxt "as code debugging" msgid "Debugging" msgstr "" -#: templates/debug.php:54, templates/debug.php:262, templates/debug.php:388, templates/debug.php:528 +#: templates/debug.php:58, templates/debug.php:279, templates/debug.php:415, templates/debug.php:580 msgid "Actions" msgstr "" -#: templates/debug.php:64 +#: templates/debug.php:68 msgid "Are you sure you want to delete all Freemius data?" msgstr "" -#: templates/debug.php:64 +#: templates/debug.php:68 msgid "Delete All Accounts" msgstr "" -#: templates/debug.php:71 +#: templates/debug.php:75 msgid "Clear API Cache" msgstr "" -#: templates/debug.php:79 +#: templates/debug.php:83 msgid "Clear Updates Transients" msgstr "" -#: templates/debug.php:88 +#: templates/debug.php:92 msgid "Reset Deactivation Snoozing" msgstr "" -#: templates/debug.php:96 +#: templates/debug.php:100 msgid "Sync Data From Server" msgstr "" -#: templates/debug.php:105 +#: templates/debug.php:109 msgid "Migrate Options to Network" msgstr "" -#: templates/debug.php:110 +#: templates/debug.php:114 msgid "Load DB Option" msgstr "" -#: templates/debug.php:113 +#: templates/debug.php:117 msgid "Set DB Option" msgstr "" -#: templates/debug.php:194 +#: templates/debug.php:211 msgid "Key" msgstr "" -#: templates/debug.php:195 +#: templates/debug.php:212 msgid "Value" msgstr "" -#: templates/debug.php:211 +#: templates/debug.php:228 msgctxt "as software development kit versions" msgid "SDK Versions" msgstr "" -#: templates/debug.php:216 +#: templates/debug.php:233 msgid "SDK Path" msgstr "" -#: templates/debug.php:217, templates/debug.php:256 +#: templates/debug.php:234, templates/debug.php:273 msgid "Module Path" msgstr "" -#: templates/debug.php:218 +#: templates/debug.php:235 msgid "Is Active" msgstr "" -#: templates/debug.php:246, templates/debug/plugins-themes-sync.php:35 +#: templates/debug.php:263, templates/debug/plugins-themes-sync.php:35 msgid "Plugins" msgstr "" -#: templates/debug.php:246, templates/debug/plugins-themes-sync.php:56 +#: templates/debug.php:263, templates/debug/plugins-themes-sync.php:56 msgid "Themes" msgstr "" -#: templates/debug.php:251, templates/debug.php:382, templates/debug.php:465, templates/debug/scheduled-crons.php:80 +#: templates/debug.php:268, templates/debug.php:409, templates/debug.php:517, templates/debug/scheduled-crons.php:80 msgid "Slug" msgstr "" -#: templates/debug.php:253, templates/debug.php:464 +#: templates/debug.php:270, templates/debug.php:516 msgid "Title" msgstr "" -#: templates/debug.php:254 +#: templates/debug.php:271 msgctxt "as application program interface" msgid "API" msgstr "" -#: templates/debug.php:255 +#: templates/debug.php:272 msgid "Freemius State" msgstr "" -#: templates/debug.php:259 +#: templates/debug.php:276 msgid "Network Blog" msgstr "" -#: templates/debug.php:260 +#: templates/debug.php:277 msgid "Network User" msgstr "" -#: templates/debug.php:297 +#: templates/debug.php:323 msgctxt "as connection was successful" msgid "Connected" msgstr "" -#: templates/debug.php:298 +#: templates/debug.php:325 msgctxt "as connection blocked" msgid "Blocked" msgstr "" -#: templates/debug.php:334 +#: templates/debug.php:326 +msgctxt "API connectivity state is unknown" +msgid "Unknown" +msgstr "" + +#: templates/debug.php:362 msgid "Simulate Trial Promotion" msgstr "" -#: templates/debug.php:346 +#: templates/debug.php:374 msgid "Simulate Network Upgrade" msgstr "" -#. translators: %s: 'plugin' or 'theme' -#: templates/debug.php:371 +#: templates/debug.php:398 msgid "%s Installs" msgstr "" -#: templates/debug.php:373 +#: templates/debug.php:400 msgctxt "like websites" msgid "Sites" msgstr "" -#: templates/debug.php:379, templates/account/partials/site.php:156 +#: templates/debug.php:406, templates/account/partials/site.php:156 msgid "Blog ID" msgstr "" -#: templates/debug.php:384 +#: templates/debug.php:411 msgid "License ID" msgstr "" -#: templates/debug.php:445, templates/debug.php:551, templates/account/partials/addon.php:440 +#: templates/debug.php:497, templates/debug.php:603, templates/account/partials/addon.php:440 msgctxt "verb" msgid "Delete" msgstr "" -#: templates/debug.php:459 +#: templates/debug.php:511 msgid "Add Ons of module %s" msgstr "" -#: templates/debug.php:518 +#: templates/debug.php:570 msgid "Users" msgstr "" -#: templates/debug.php:525 +#: templates/debug.php:577 msgid "Verified" msgstr "" -#: templates/debug.php:567 +#: templates/debug.php:619 msgid "%s Licenses" msgstr "" -#: templates/debug.php:572 +#: templates/debug.php:624 msgid "Plugin ID" msgstr "" -#: templates/debug.php:574 +#: templates/debug.php:626 msgid "Plan ID" msgstr "" -#: templates/debug.php:575 +#: templates/debug.php:627 msgid "Quota" msgstr "" -#: templates/debug.php:576 +#: templates/debug.php:628 msgid "Activated" msgstr "" -#: templates/debug.php:577 +#: templates/debug.php:629 msgid "Blocking" msgstr "" -#: templates/debug.php:578, templates/debug.php:649, templates/debug/logger.php:22 +#: templates/debug.php:630, templates/debug.php:701, templates/debug/logger.php:22 msgid "Type" msgstr "" -#: templates/debug.php:580 +#: templates/debug.php:632 msgctxt "as expiration date" msgid "Expiration" msgstr "" -#: templates/debug.php:608 +#: templates/debug.php:660 msgid "Debug Log" msgstr "" -#: templates/debug.php:612 +#: templates/debug.php:664 msgid "All Types" msgstr "" -#: templates/debug.php:619 +#: templates/debug.php:671 msgid "All Requests" msgstr "" -#: templates/debug.php:624, templates/debug.php:653, templates/debug/logger.php:25 +#: templates/debug.php:676, templates/debug.php:705, templates/debug/logger.php:25 msgid "File" msgstr "" -#: templates/debug.php:625, templates/debug.php:651, templates/debug/logger.php:23 +#: templates/debug.php:677, templates/debug.php:703, templates/debug/logger.php:23 msgid "Function" msgstr "" -#: templates/debug.php:626 +#: templates/debug.php:678 msgid "Process ID" msgstr "" -#: templates/debug.php:627 +#: templates/debug.php:679 msgid "Logger" msgstr "" -#: templates/debug.php:628, templates/debug.php:652, templates/debug/logger.php:24 +#: templates/debug.php:680, templates/debug.php:704, templates/debug/logger.php:24 msgid "Message" msgstr "" -#: templates/debug.php:630 +#: templates/debug.php:682 msgid "Filter" msgstr "" -#: templates/debug.php:638 +#: templates/debug.php:690 msgid "Download" msgstr "" -#: templates/debug.php:654, templates/debug/logger.php:26 +#: templates/debug.php:706, templates/debug/logger.php:26 msgid "Timestamp" msgstr "" +#: templates/secure-https-header.php:28 +msgid "Secure HTTPS %s page, running from an external domain" +msgstr "" + +#: includes/customizer/class-fs-customizer-support-section.php:55, templates/plugin-info/features.php:43 +msgid "Support" +msgstr "" + #: includes/debug/class-fs-debug-bar-panel.php:48, templates/debug/api-calls.php:54, templates/debug/logger.php:62 msgctxt "milliseconds" msgid "ms" @@ -1163,120 +1940,211 @@ msgstr "" msgid "Requests" msgstr "" -#: includes/managers/class-fs-clone-manager.php:703 +#: includes/managers/class-fs-clone-manager.php:839 msgid "Invalid clone resolution action." msgstr "" -#: includes/managers/class-fs-clone-manager.php:851 +#: includes/managers/class-fs-clone-manager.php:1024 msgid "products" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1039 -msgid "The products below have been placed into safe mode because we noticed that %2$s is an exact copy of %3$s:%1$s" +#: includes/managers/class-fs-clone-manager.php:1205 +msgid "%1$s has been placed into safe mode because we noticed that %2$s is an exact copy of %3$s." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1040 -msgid "The products below have been placed into safe mode because we noticed that %2$s is an exact copy of these sites:%3$s%1$s" +#: includes/managers/class-fs-clone-manager.php:1211 +msgid "The products below have been placed into safe mode because we noticed that %2$s is an exact copy of %3$s:%1$s" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1033 -msgid "%1$s has been placed into safe mode because we noticed that %2$s is an exact copy of %3$s." +#: includes/managers/class-fs-clone-manager.php:1212 +msgid "The products below have been placed into safe mode because we noticed that %2$s is an exact copy of these sites:%3$s%1$s" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1066 +#: includes/managers/class-fs-clone-manager.php:1238 msgid "the above-mentioned sites" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1079 +#: includes/managers/class-fs-clone-manager.php:1251 msgid "Is %2$s a duplicate of %4$s?" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1080 +#: includes/managers/class-fs-clone-manager.php:1252 msgid "Yes, %2$s is a duplicate of %4$s for the purpose of testing, staging, or development." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1085 +#: includes/managers/class-fs-clone-manager.php:1257 msgid "Long-Term Duplicate" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1090 +#: includes/managers/class-fs-clone-manager.php:1262 msgid "Duplicate Website" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1096 +#: includes/managers/class-fs-clone-manager.php:1268 msgid "Is %2$s the new home of %4$s?" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1098 +#: includes/managers/class-fs-clone-manager.php:1270 msgid "Yes, %%2$s is replacing %%4$s. I would like to migrate my %s from %%4$s to %%2$s." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1099, templates/forms/subscription-cancellation.php:52 +#: includes/managers/class-fs-clone-manager.php:1271, templates/forms/subscription-cancellation.php:52 msgid "license" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1099 +#: includes/managers/class-fs-clone-manager.php:1271 msgid "data" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1105 +#: includes/managers/class-fs-clone-manager.php:1277 msgid "Migrate License" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1106 +#: includes/managers/class-fs-clone-manager.php:1278 msgid "Migrate" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1112 +#: includes/managers/class-fs-clone-manager.php:1284 msgid "Is %2$s a new website?" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1113 +#: includes/managers/class-fs-clone-manager.php:1285 msgid "Yes, %2$s is a new and different website that is separate from %4$s." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1115 +#: includes/managers/class-fs-clone-manager.php:1287 msgid "It requires license activation." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1122 +#: includes/managers/class-fs-clone-manager.php:1294 msgid "New Website" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1145 +#: includes/managers/class-fs-clone-manager.php:1319 msgctxt "Clone resolution admin notice products list label" msgid "Products" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1230 +#: includes/managers/class-fs-clone-manager.php:1408 msgid "You marked this website, %s, as a temporary duplicate of %s." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1231 +#: includes/managers/class-fs-clone-manager.php:1409 msgid "You marked this website, %s, as a temporary duplicate of these sites" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1245 +#: includes/managers/class-fs-clone-manager.php:1423 msgid "%s automatic security & feature updates and paid functionality will keep working without interruptions until %s (or when your license expires, whatever comes first)." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1248 +#: includes/managers/class-fs-clone-manager.php:1426 msgctxt "\"The \", e.g.: \"The plugin\"" msgid "The %s's" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1251 +#: includes/managers/class-fs-clone-manager.php:1429 msgid "The following products'" msgstr "" -#: includes/managers/class-fs-clone-manager.php:1259 +#: includes/managers/class-fs-clone-manager.php:1437 msgid "If this is a long term duplicate, to keep automatic updates and paid functionality after %s, please %s." msgstr "" -#: includes/managers/class-fs-clone-manager.php:1261 +#: includes/managers/class-fs-clone-manager.php:1439 msgid "activate a license here" msgstr "" +#: includes/managers/class-fs-permission-manager.php:191 +msgid "View Basic Website Info" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:192 +msgid "Homepage URL & title, WP & PHP versions, and site language" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:195 +msgid "To provide additional functionality that's relevant to your website, avoid WordPress or PHP version incompatibilities that can break your website, and recognize which languages & regions the %s should be translated and tailored to." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:207 +msgid "View Basic %s Info" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:210 +msgid "Current %s & SDK versions, and if active or uninstalled" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:261 +msgid "View License Essentials" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:262 +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:272 +msgid "To let you manage & control where the license is activated and ensure %s security & feature updates are only delivered to websites you authorize." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:284 +msgid "View %s State" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:287 +msgid "Is active, deactivated, or uninstalled" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:290 +msgid "So you can reuse the license when the %s is no longer active." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:326 +msgid "View Diagnostic Info" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:326, includes/managers/class-fs-permission-manager.php:363 +msgid "optional" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:327 +msgid "WordPress & PHP versions, site language & title" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:330 +msgid "To avoid breaking your website due to WordPress or PHP version incompatibilities, and recognize which languages & regions the %s should be translated and tailored to." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:363 +msgid "View Plugins & Themes List" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:364 +msgid "Names, slugs, versions, and if active or not" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:365 +msgid "To ensure compatibility and avoid conflicts with your installed plugins and themes." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:382 +msgid "View Basic Profile Info" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:383 +msgid "Your WordPress user's: first & last name, and email address" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:384 +msgid "Never miss important updates, get security warnings before they become public knowledge, and receive notifications about special offers and awesome new features." +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:405 +msgid "Newsletter" +msgstr "" + +#: includes/managers/class-fs-permission-manager.php:406 +msgid "Updates, announcements, marketing, no spam" +msgstr "" + #: templates/account/billing.php:22 msgctxt "verb" msgid "Update" @@ -1342,6 +2210,16 @@ msgstr "" msgid "Invoice" msgstr "" +#: templates/connect/permissions-group.php:31, templates/forms/optout.php:26, templates/js/permissions.php:78 +msgctxt "verb" +msgid "Opt Out" +msgstr "" + +#: templates/connect/permissions-group.php:32, templates/js/permissions.php:77 +msgctxt "verb" +msgid "Opt In" +msgstr "" + #: templates/debug/api-calls.php:56 msgid "API" msgstr "" @@ -1438,24 +2316,24 @@ msgstr "" msgid "Apply to become an affiliate" msgstr "" -#: templates/forms/affiliation.php:132 -msgid "Due to violation of our affiliation terms, we decided to temporarily block your affiliation account. If you have any questions, please contact support." +#: templates/forms/affiliation.php:108 +msgid "Your affiliate application for %s has been accepted! Log in to your affiliate area at: %s." msgstr "" -#: templates/forms/affiliation.php:129 -msgid "Thank you for applying for our affiliate program, unfortunately, we've decided at this point to reject your application. Please try again in 30 days." +#: templates/forms/affiliation.php:123 +msgid "Thank you for applying for our affiliate program, we'll review your details during the next 14 days and will get back to you with further information." msgstr "" #: templates/forms/affiliation.php:126 msgid "Your affiliation account was temporarily suspended." msgstr "" -#: templates/forms/affiliation.php:123 -msgid "Thank you for applying for our affiliate program, we'll review your details during the next 14 days and will get back to you with further information." +#: templates/forms/affiliation.php:129 +msgid "Thank you for applying for our affiliate program, unfortunately, we've decided at this point to reject your application. Please try again in 30 days." msgstr "" -#: templates/forms/affiliation.php:108 -msgid "Your affiliate application for %s has been accepted! Log in to your affiliate area at: %s." +#: templates/forms/affiliation.php:132 +msgid "Due to violation of our affiliation terms, we decided to temporarily block your affiliation account. If you have any questions, please contact support." msgstr "" #: templates/forms/affiliation.php:145 @@ -1562,7 +2440,7 @@ msgstr "" msgid "Please provide details on how you intend to promote %s (please be as specific as possible)." msgstr "" -#: templates/forms/affiliation.php:233, templates/forms/resend-key.php:22 +#: templates/forms/affiliation.php:233, templates/forms/resend-key.php:22, templates/account/partials/disconnect-button.php:92 msgid "Cancel" msgstr "" @@ -1586,10 +2464,6 @@ msgstr "" msgid "User key" msgstr "" -#: templates/forms/data-debug-mode.php:162 -msgid "An unknown error has occurred." -msgstr "" - #: templates/forms/email-address-update.php:32 msgid "Email address update" msgstr "" @@ -1643,62 +2517,40 @@ msgstr "" msgid "Update License" msgstr "" -#: templates/forms/license-activation.php:41 -msgid "The %1$s will be periodically sending data to %2$s to check for security and feature updates, and verify the validity of your license." -msgstr "" - -#: templates/forms/license-activation.php:211 -msgid "Associate with the license owner's account." -msgstr "" - -#: templates/forms/optout.php:30 -msgctxt "verb" -msgid "Opt Out" +#: templates/forms/license-activation.php:34 +msgid "The %1$s will be periodically sending essential license data to %2$s to check for security and feature updates, and verify the validity of your license." msgstr "" -#: templates/forms/optout.php:31 -msgctxt "verb" -msgid "Opt In" +#: templates/forms/license-activation.php:39 +msgid "Agree & Activate License" msgstr "" -#: templates/forms/optout.php:41 -msgid "We appreciate your help in making the %s better by letting us track some usage data." +#: templates/forms/license-activation.php:204 +msgid "Associate with the license owner's account." msgstr "" #: templates/forms/optout.php:44 -msgid "Usage tracking is done in the name of making %s better. Making a better user experience, prioritizing new features, and more good things. We'd really appreciate if you'll reconsider letting us continue with the tracking." +msgid "Communication" msgstr "" -#: templates/forms/optout.php:45 -msgid "On second thought - I want to continue helping" +#: templates/forms/optout.php:56 +msgid "Stay Connected" msgstr "" -#: templates/forms/optout.php:34 -msgid "Connectivity to the licensing engine was successfully re-established. Automatic security & feature updates are now available through the WP Admin Dashboard." +#: templates/forms/optout.php:61 +msgid "Diagnostic Info" msgstr "" -#: templates/forms/optout.php:36 -msgid "Warning: Opting out will block automatic updates" +#: templates/forms/optout.php:77 +msgid "Keep Sharing" msgstr "" -#: templates/forms/optout.php:37 -msgid "Ongoing connectivity with the licensing engine is essential for receiving automatic security & feature updates of the paid product. To receive these updates, data like your license key, %1$s version, and WordPress version, is periodically sent to the server to check for updates. By opting out, you understand that your site won't receive automatic updates for %2$s from within the WP Admin Dashboard. This can put your site at risk, and we highly recommend to keep this connection active. If you do choose to opt-out, you'll need to check for %1$s updates and install them manually." +#: templates/forms/optout.php:82 +msgid "Extensions" msgstr "" -#: templates/forms/optout.php:39 -msgid "I'd like to keep automatic updates" -msgstr "" - -#: templates/forms/optout.php:49 -msgid "By clicking \"Opt Out\", we will no longer be sending any data from %s to %s." -msgstr "" - -#: templates/forms/optout.php:74 -msgid "Plugins & themes tracking" -msgstr "" - -#: templates/forms/optout.php:261 -msgid "Saved" +#: templates/forms/optout.php:104 +msgid "Keep automatic updates" msgstr "" #: templates/forms/premium-versions-upgrade-handler.php:40 @@ -1709,14 +2561,6 @@ msgstr "" msgid " %s to access version %s security & feature updates, and support." msgstr "" -#: templates/forms/premium-versions-upgrade-handler.php:46 -msgid "Renew your license now" -msgstr "" - -#: templates/forms/premium-versions-upgrade-handler.php:47 -msgid "Buy a license now" -msgstr "" - #: templates/forms/premium-versions-upgrade-handler.php:54 msgid "New Version Available" msgstr "" @@ -1730,10 +2574,6 @@ msgstr "" msgid "Send License Key" msgstr "" -#: templates/forms/resend-key.php:24, templates/forms/user-change.php:29 -msgid "Other" -msgstr "" - #: templates/forms/resend-key.php:58 msgid "Enter the email address you've used during the purchase and we will resend you the license key." msgstr "" @@ -1774,21 +2614,14 @@ msgstr "" msgid "Cancel %s & Proceed" msgstr "" -#. translators: %1$s: Number of trial days; %2$s: Plan name; #: templates/forms/trial-start.php:22 msgid "You are 1-click away from starting your %1$s-day free trial of the %2$s plan." msgstr "" -#. translators: %s: Link to freemius.com #: templates/forms/trial-start.php:28 msgid "For compliance with the WordPress.org guidelines, before we start the trial we ask that you opt in with your user and non-sensitive site information, allowing the %s to periodically send data to %s to check for version updates and to validate your trial." msgstr "" -#: templates/forms/trial-start.php:53 -msgctxt "call to action" -msgid "Start free trial" -msgstr "" - #: templates/forms/user-change.php:26 msgid "By changing the user, you agree to transfer the account ownership to:" msgstr "" @@ -1801,12 +2634,16 @@ msgstr "" msgid "Enter email address" msgstr "" -#: templates/partials/network-activation.php:36 -msgid "Activate license on all pending sites." +#: templates/js/permissions.php:337, templates/js/permissions.php:485 +msgid "Saved" msgstr "" -#: templates/partials/network-activation.php:37 -msgid "Apply on all pending sites." +#: templates/js/style-premium-theme.php:39 +msgid "Premium" +msgstr "" + +#: templates/js/style-premium-theme.php:42 +msgid "Beta" msgstr "" #: templates/partials/network-activation.php:32 @@ -1817,6 +2654,14 @@ msgstr "" msgid "Apply on all sites in the network." msgstr "" +#: templates/partials/network-activation.php:36 +msgid "Activate license on all pending sites." +msgstr "" + +#: templates/partials/network-activation.php:37 +msgid "Apply on all pending sites." +msgstr "" + #: templates/partials/network-activation.php:45, templates/partials/network-activation.php:79 msgid "allow" msgstr "" @@ -1833,10 +2678,6 @@ msgstr "" msgid "Click to view full-size screenshot %d" msgstr "" -#: templates/plugin-info/features.php:43 -msgid "Support" -msgstr "" - #: templates/plugin-info/features.php:56 msgid "Unlimited Updates" msgstr "" @@ -1859,35 +2700,43 @@ msgstr "" msgid "%1$s will immediately stop all future recurring payments and your %s plan license will expire in %s." msgstr "" +#: templates/account/partials/addon.php:190 +msgid "Cancelled" +msgstr "" + #: templates/account/partials/addon.php:200 msgid "No expiration" msgstr "" -#: templates/account/partials/addon.php:190 -msgid "Cancelled" +#: templates/account/partials/disconnect-button.php:74 +msgid "By disconnecting the website, previously shared diagnostic data about %1$s will be deleted and no longer visible to %2$s." msgstr "" -#: templates/account/partials/site.php:49, templates/account/partials/site.php:169 -msgid "Opt In" +#: templates/account/partials/disconnect-button.php:78 +msgid "Disconnecting the website will permanently remove %s from your User Dashboard's account." msgstr "" -#: templates/account/partials/site.php:169 -msgid "Opt Out" +#: templates/account/partials/disconnect-button.php:84 +msgid "If you wish to cancel your %1$s plan's subscription instead, please navigate to the %2$s and cancel it there." +msgstr "" + +#: templates/account/partials/disconnect-button.php:88 +msgid "Are you sure you would like to proceed with the disconnection?" msgstr "" -#: templates/account/partials/site.php:189 +#: templates/account/partials/site.php:190 msgid "Owner Name" msgstr "" -#: templates/account/partials/site.php:201 +#: templates/account/partials/site.php:202 msgid "Owner Email" msgstr "" -#: templates/account/partials/site.php:213 +#: templates/account/partials/site.php:214 msgid "Owner ID" msgstr "" -#: templates/account/partials/site.php:286 +#: templates/account/partials/site.php:288 msgid "Subscription" msgstr "" diff --git a/freemius/require.php b/freemius/require.php index fcbb880..c1f8366 100644 --- a/freemius/require.php +++ b/freemius/require.php @@ -13,6 +13,7 @@ // Configuration should be loaded first. require_once dirname( __FILE__ ) . '/config.php'; require_once WP_FS__DIR_INCLUDES . '/fs-core-functions.php'; + require_once WP_FS__DIR_INCLUDES . '/fs-html-escaping-functions.php'; // Logger must be loaded before any other. require_once WP_FS__DIR_INCLUDES . '/class-fs-logger.php'; @@ -21,6 +22,8 @@ // require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-abstract-manager.php'; require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-option-manager.php'; require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-gdpr-manager.php'; + require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-clone-manager.php'; + require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-permission-manager.php'; require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-cache-manager.php'; require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-admin-notice-manager.php'; require_once WP_FS__DIR_INCLUDES . '/managers/class-fs-admin-menu-manager.php'; diff --git a/freemius/start.php b/freemius/start.php index 89bbfb0..096f510 100644 --- a/freemius/start.php +++ b/freemius/start.php @@ -15,7 +15,7 @@ * * @var string */ - $this_sdk_version = '2.4.5'; + $this_sdk_version = '2.5.10'; #region SDK Selection Logic -------------------------------------------------------------------- @@ -68,7 +68,11 @@ if ( ! isset( $fs_active_plugins ) ) { // Load all Freemius powered active plugins. - $fs_active_plugins = get_option( 'fs_active_plugins', new stdClass() ); + $fs_active_plugins = get_option( 'fs_active_plugins' ); + + if ( ! is_object( $fs_active_plugins ) ) { + $fs_active_plugins = new stdClass(); + } if ( ! isset( $fs_active_plugins->plugins ) ) { $fs_active_plugins->plugins = array(); diff --git a/freemius/templates/account.php b/freemius/templates/account.php index 0921366..ca27d07 100644 --- a/freemius/templates/account.php +++ b/freemius/templates/account.php @@ -46,6 +46,7 @@ $site = $fs->get_site(); $name = $user->get_name(); $license = $fs->_get_license(); + $is_license_foreign = ( is_object( $license ) && $user->id != $license->user_id ); $is_data_debug_mode = $fs->is_data_debug_mode(); $is_whitelabeled = $fs->is_whitelabeled(); $subscription = ( is_object( $license ) ? @@ -58,10 +59,19 @@ $show_upgrade = ( ! $is_whitelabeled && $has_paid_plan && ! $is_paying && ! $is_paid_trial ); $trial_plan = $fs->get_trial_plan(); + $is_plan_change_supported = ( + ! $fs->is_single_plan() && + ! $fs->apply_filters( 'hide_plan_change', false ) + ); + if ( $has_paid_plan ) { $fs->_add_license_activation_dialog_box(); } + if ( $fs->should_handle_user_change() ) { + $fs->_add_email_address_update_dialog_box(); + } + $ids_of_installs_activated_with_foreign_licenses = $fs->should_handle_user_change() ? $fs->get_installs_ids_with_foreign_licenses() : array(); @@ -86,16 +96,15 @@ ) ); } - $payments = $fs->_fetch_payments(); - - $show_billing = ( ! $is_whitelabeled && is_array( $payments ) && 0 < count( $payments ) ); + $show_billing = ( ! $is_whitelabeled && ! $fs->apply_filters( 'hide_billing_and_payments_info', false ) ); + if ( $show_billing ) { + $payments = $fs->_fetch_payments(); + $show_billing = ( is_array( $payments ) && 0 < count( $payments ) ); + } - $has_tabs = $fs->_add_tabs_before_content(); - if ( $has_tabs ) { - $query_params['tabs'] = 'true'; - } + $has_tabs = $fs->_add_tabs_before_content(); // Aliases. $download_latest_text = fs_text_x_inline( 'Download Latest', 'as download latest version', 'download-latest', $slug ); @@ -140,6 +149,7 @@ $install = $fs->get_install_by_blog_id( $site_info['blog_id'] ); $view_params = array( 'freemius' => $fs, + 'user' => $fs->get_user(), 'license' => $license, 'site' => $site_info, 'install' => $install, @@ -234,20 +244,28 @@ $addons_to_show = array_unique( array_merge( $installed_addons_ids, $account_addons ) ); $is_active_bundle_subscription = ( is_object( $bundle_subscription ) && $bundle_subscription->is_active() ); + + $available_license = ( $fs->is_free_plan() && ! fs_is_network_admin() ) ? + $fs->_get_available_premium_license( $site->is_localhost() ) : + null; + + $available_license_paid_plan = is_object( $available_license ) ? + $fs->_get_plan_by_id( $available_license->plan_id ) : + null; ?>
    apply_filters( 'hide_account_tabs', false ) ) : ?> @@ -274,31 +292,26 @@ class="nav-tab">
  •  • 
  • -
  • +
  •  • 
  • -
    - - - -
    + $fs, + 'license' => $available_license, + 'license_paid_plan' => $available_license_paid_plan, + ); + fs_require_template( 'account/partials/disconnect-button.php', $view_params ); ?>
  •  • 
  • -
    +
  • - +
  •  • 
  • - is_single_plan() ) : ?> +
  • -
  •  • 
  • - +
  • - - + + get_unique_affix() . '_sync_license' ) ?> @@ -366,7 +379,7 @@ class="dashicons dashicons-image-rotate"> 'user_name', 'title' => fs_text_inline( 'Name', 'name', $slug ), @@ -435,7 +448,7 @@ class="dashicons dashicons-image-rotate"> $fs->get_plugin_version() ); - if ( ! fs_is_network_admin() && $is_premium && ! $is_whitelabeled ) { + if ( ! fs_is_network_admin() && $is_premium ) { $profile[] = array( 'id' => 'beta_program', 'title' => '', @@ -495,9 +508,9 @@ class="dashicons dashicons-image-rotate"> - + - + > @@ -537,33 +550,31 @@ class="fs-tag ">
    - is_free_plan() && ! fs_is_network_admin() ? $fs->_get_available_premium_license( $site->is_localhost() ) : false ?> - _get_plan_by_id( $available_license->plan_id ) ?> $fs, 'slug' => $slug, 'license' => $available_license, - 'plan' => $premium_plan, + 'plan' => $available_license_paid_plan, 'is_localhost' => $site->is_localhost(), 'install_id' => $site->id, 'class' => 'button-primary', ); fs_require_template( 'account/partials/activate-license-button.php', $view_params ); ?> - - + + value="get_unique_affix() ) ?>_sync_license"> get_unique_affix() . '_sync_license' ) ?> - is_single_plan() ) : ?> - + can_use_premium_code() ? 'success' : 'warn' ?>" - + @@ -658,6 +669,7 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>" 'user_name' ) ) ) ) : ?> + can_use_premium_code() ? 'success' : 'warn' ?>" - + @@ -730,12 +743,30 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>"
    + } + + /** + * It's possible for the `Freemius::switch_to_blog()` method to be called within the `site.php` template and this changes the Freemius instance's context, so this check is for restoring the previous context based on the previously retrieved site. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + $current_install = $fs->get_site(); + + if ( + is_object( $site ) && + ( ! is_object( $current_install ) || $current_install->id != $site->id ) + ) { + $fs->switch_to_blog( $current_blog_id, $site, true ); + } + ?>
    @@ -839,7 +870,7 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>" $VARS['id'] ); + $view_params = array( 'id' => $VARS['id'], 'payments' => $payments ); fs_require_once_template( 'account/billing.php', $view_params ); fs_require_once_template( 'account/payments.php', $view_params ); } @@ -919,7 +950,7 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>" if ( ! isChecked || confirm( '' ) ) { $.ajax( { - url : ajaxurl, + url : , method: 'POST', data : { action : 'get_ajax_action( 'set_beta_mode' ) ?>', @@ -1062,7 +1093,7 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>" var $toggleLink = $( this ); $.ajax( { - url : ajaxurl, + url : , method: 'POST', data : { action : 'get_ajax_action( 'toggle_whitelabel_mode' ) ?>', @@ -1095,4 +1126,4 @@ class="fs-tag fs-can_use_premium_code() ? 'success' : 'warn' ?>" 'module_slug' => $slug, 'module_version' => $fs->get_plugin_version(), ); - fs_require_template( 'powered-by.php', $params ); \ No newline at end of file + fs_require_template( 'powered-by.php', $params ); diff --git a/freemius/templates/account/billing.php b/freemius/templates/account/billing.php index a4de409..a64183f 100644 --- a/freemius/templates/account/billing.php +++ b/freemius/templates/account/billing.php @@ -35,16 +35,16 @@ echo ' class="fs-read-mode"'; } ?>> - - + + - - + + - - + + $country ) : ?> + value="" address_country_code, $code ) ?>> + @@ -377,12 +377,12 @@ class="button">, method : 'POST', data : { - action : 'get_ajax_action( 'update_billing' ) ?>', - security : 'get_ajax_security( 'update_billing' ) ?>', - module_id: 'get_id() ?>', + action : get_ajax_action( 'update_billing' ) ) ?>, + security : get_ajax_security( 'update_billing' ) ) ?>, + module_id: get_id() ) ?>, billing : billing }, success: function (resultObj) { diff --git a/freemius/templates/account/partials/disconnect-button.php b/freemius/templates/account/partials/disconnect-button.php new file mode 100644 index 0000000..0f4725a --- /dev/null +++ b/freemius/templates/account/partials/disconnect-button.php @@ -0,0 +1,104 @@ +_get_subscription( $license->id ) : + null; + + $has_active_subscription = ( + is_object( $license_subscription ) && + $license_subscription->is_active() + ); + + $button_id = "fs_disconnect_button_{$fs->get_id()}"; + + $website_link = sprintf( '%s', fs_strip_url_protocol( untrailingslashit( Freemius::get_unfiltered_site_url() ) ) ); +?> + + +
    + + + + + + esc_html_inline( 'Disconnect', 'disconnect' ) ?> + +
    \ No newline at end of file diff --git a/freemius/templates/account/partials/site.php b/freemius/templates/account/partials/site.php index fd9b237..a60f5d0 100644 --- a/freemius/templates/account/partials/site.php +++ b/freemius/templates/account/partials/site.php @@ -23,7 +23,7 @@ $is_whitelabeled = $fs->is_whitelabeled(); $has_paid_plan = $fs->has_paid_plan(); $is_premium = $fs->is_premium(); - $main_user = $fs->get_user(); + $main_user = $VARS['user']; $blog_id = $site['blog_id']; $install = $VARS['install']; @@ -32,7 +32,7 @@ $trial_plan = $fs->get_trial_plan(); $free_text = fs_text_inline( 'Free', 'free', $slug ); - if ( $is_whitelabeled && $fs->is_delegated_connection( $blog_id ) ) { + if ( $is_whitelabeled && is_object( $install ) && $fs->is_delegated_connection( $blog_id ) ) { $is_whitelabeled = $fs->is_whitelabeled( true, $blog_id ); } ?> @@ -159,14 +159,15 @@ class="dashicons dashicons-arrow-right-alt2"> license_id ) ) : ?> + is_homepage_url_tracking_allowed( $blog_id ) ?>
    id}", ':' ) ) ?> - + } ?>" data-is-disconnected="">
    @@ -174,7 +175,7 @@ class="dashicons dashicons-arrow-right-alt2"> - user_id != $main_user->id ) : ?> + user_id != $main_user->id ) : ?> : - public_key ) ?> + public_key ) ?> + diff --git a/freemius/templates/account/payments.php b/freemius/templates/account/payments.php index fd54c9b..c2fb1a7 100644 --- a/freemius/templates/account/payments.php +++ b/freemius/templates/account/payments.php @@ -10,19 +10,19 @@ exit; } - /** - * @var array $VARS - * @var Freemius $fs - */ - $fs = freemius( $VARS['id'] ); + /** + * @var array $VARS + * @var Freemius $fs + */ + $fs = freemius( $VARS['id'] ); - $slug = $fs->get_slug(); + /** + * @var FS_Payment[] $payments + */ + $payments = $VARS['payments']; - $payments = $fs->_fetch_payments(); - - $show_payments = ( is_array( $payments ) && 0 < count( $payments ) ); + $slug = $fs->get_slug(); - if ( $show_payments ) : ?>
    @@ -56,4 +56,3 @@ class="button button-small"
    - data-id="" data-manager-id="" data-slug="" data-type="" - class=" fs-notice"> - +
    > + + - -
    + + +
    + +
    +
    - - + + + + +
    diff --git a/freemius/templates/api-connectivity-message-js.php b/freemius/templates/api-connectivity-message-js.php new file mode 100644 index 0000000..f5f8658 --- /dev/null +++ b/freemius/templates/api-connectivity-message-js.php @@ -0,0 +1,32 @@ + + \ No newline at end of file diff --git a/freemius/templates/auto-installation.php b/freemius/templates/auto-installation.php index 6b8183c..23bbf41 100644 --- a/freemius/templates/auto-installation.php +++ b/freemius/templates/auto-installation.php @@ -164,7 +164,7 @@ class="button button-primary">, method : 'POST', data : data, success: function (resultObj) { diff --git a/freemius/templates/checkout.php b/freemius/templates/checkout.php index a0969cc..3e8da51 100644 --- a/freemius/templates/checkout.php +++ b/freemius/templates/checkout.php @@ -60,6 +60,7 @@ 'plugin_version' => $fs->get_plugin_version(), 'mode' => 'dashboard', 'trial' => fs_request_get_bool( 'trial' ), + 'is_ms' => ( fs_is_network_admin() && $fs->is_network_active() ), ); $plan_id = fs_request_get( 'plan_id' ); @@ -277,16 +278,18 @@ FS.PostMessage.receiveOnce('pending_activation', function (data) { var requestData = { - user_email: data.user_email + user_email : data.user_email, + support_email_address: data.support_email_address }; if (true === data.auto_install) requestData.auto_install = true; $.form('_get_admin_page_url( 'account', array( - 'fs_action' => $fs->get_unique_affix() . '_activate_new', - 'plugin_id' => $plugin_id, - 'pending_activation' => true, + 'fs_action' => $fs->get_unique_affix() . '_activate_new', + 'plugin_id' => $plugin_id, + 'pending_activation' => true, + 'has_upgrade_context' => true, ) ), $fs->get_unique_affix() . '_activate_new' ) ?>', requestData).submit(); }); diff --git a/freemius/templates/clone-resolution-js.php b/freemius/templates/clone-resolution-js.php new file mode 100644 index 0000000..e8f4e9b --- /dev/null +++ b/freemius/templates/clone-resolution-js.php @@ -0,0 +1,79 @@ + + \ No newline at end of file diff --git a/freemius/templates/connect.php b/freemius/templates/connect.php index 9cc7cab..3bf4ca3 100644 --- a/freemius/templates/connect.php +++ b/freemius/templates/connect.php @@ -25,6 +25,15 @@ $fs->_enqueue_connect_essentials(); + /** + * Enqueueing the styles in `_enqueue_connect_essentials()` is too late, as we need them in the HEADER. Therefore, inject the styles inline to avoid FOUC. + * + * @author Vova Feldman (@svovaf) + */ + echo "\n"; + $current_user = Freemius::_get_current_wp_user(); $first_name = $current_user->user_firstname; @@ -32,34 +41,31 @@ $first_name = $current_user->nickname; } - $site_url = get_site_url(); + $site_url = Freemius::get_unfiltered_site_url(); $protocol_pos = strpos( $site_url, '://' ); if ( false !== $protocol_pos ) { $site_url = substr( $site_url, $protocol_pos + 3 ); } - $freemius_site_www = 'https://freemius.com'; - $freemius_usage_tracking_url = $fs->get_usage_tracking_terms_url(); $freemius_plugin_terms_url = $fs->get_eula_url(); - $freemius_site_url = $fs->is_premium() ? - $freemius_site_www : - $freemius_usage_tracking_url; - - if ( $fs->is_premium() ) { - $freemius_site_url .= '?' . http_build_query( array( - 'id' => $fs->get_id(), - 'slug' => $slug, - ) ); - } - - $freemius_link = 'freemius.com'; - $error = fs_request_get( 'error' ); + $has_release_on_freemius = $fs->has_release_on_freemius(); + $require_license_key = $is_premium_only || - ( $is_freemium && $is_premium_code && fs_request_get_bool( 'require_license', true ) ); + ( + $is_freemium && + ( $is_premium_code || ! $has_release_on_freemius ) && + fs_request_get_bool( 'require_license', ( $is_premium_code || $has_release_on_freemius ) ) + ); + + $freemius_activation_terms_url = ($fs->is_premium() && $require_license_key) ? + $fs->get_license_activation_terms_url() : + $freemius_usage_tracking_url; + + $freemius_activation_terms_html = 'freemius.com'; if ( $is_pending_activation ) { $require_license_key = false; @@ -111,13 +117,13 @@ /* translators: %s: name (e.g. Hey John,) */ $hey_x_text = esc_html( sprintf( fs_text_x_inline( 'Hey %s,', 'greeting', 'hey-x', $slug ), $first_name ) ); - $is_gdpr_required = ( ! $is_pending_activation && ! $require_license_key ) ? - FS_GDPR_Manager::instance()->is_required() : - false; - - if ( is_null( $is_gdpr_required ) ) { - $is_gdpr_required = $fs->fetch_and_store_current_user_gdpr_anonymously(); - } + $activation_state = array( + 'is_license_activation' => $require_license_key, + 'is_pending_activation' => $is_pending_activation, + 'is_gdpr_required' => true, + 'is_network_level_activation' => $is_network_level_activation, + 'is_dialog' => $is_optin_dialog, + ); ?> @@ -138,26 +144,54 @@ * @author Vova Feldman * @since 2.3.2 */ - $fs->do_action( 'connect/before' ); + $fs->do_action( 'connect/before', $activation_state ); ?>
    -
    - - - $fs->get_id() ); - fs_require_once_template( 'plugin-icon.php', $vars ); - ?> - - -
    +
    + + $fs->get_id(), + 'size' => $size, + ); + + fs_require_once_template( 'plugin-icon.php', $vars ); + ?> + +
    +
    + do_action( 'connect/before_message', $activation_state ) ?> + -

    +
    apply_filters( 'connect_error_esc_html', esc_html( $error ) ) ?>
    + is_plugin_update() ) { + echo $fs->apply_filters( 'connect-header', sprintf( + '

    %s

    ', + esc_html( fs_text_inline( 'Never miss an important update', 'connect-header' ) ) + ) ); + } else { + echo $fs->apply_filters( 'connect-header_on-update', sprintf( + '

    %s

    ', + sprintf( + esc_html( + /* translators: %1$s: plugin name (e.g., "Awesome Plugin"); %2$s: version (e.g., "1.2.3") */ + fs_text_inline('Thank you for updating to %1$s v%2$s!', 'connect-header_on-update' ) + ), + esc_html( $fs->get_plugin_name() ), + $fs->get_plugin_version() + ) + ) ); + } + } + ?>

    is_enable_anonymous() $message = $fs->apply_filters( 'pending_activation_message', sprintf( /* translators: %s: name (e.g. Thanks John!) */ fs_text_inline( 'Thanks %s!', 'thanks-x', $slug ) . '
    ' . - fs_text_inline( 'You should receive an activation email for %s to your mailbox at %s. Please make sure you click the activation button in that email to %s.', 'pending-activation-message', $slug ), + fs_text_inline( 'You should receive a confirmation email for %s to your mailbox at %s. Please make sure you click the button in that email to %s.', 'pending-activation-message', $slug ), $first_name, '' . $fs->get_plugin_name() . '', '' . $current_user->user_email . '', - fs_text_inline( 'complete the install', 'complete-the-install', $slug ) + fs_text_inline( 'complete the opt-in', 'complete-the-opt-in', $slug ) ) ); } else if ( $require_license_key ) { - $button_label = $is_network_upgrade_mode ? - fs_text_inline( 'Activate License', 'agree-activate-license', $slug ) : - fs_text_inline( 'Agree & Activate License', 'agree-activate-license', $slug ); + $button_label = fs_text_inline( 'Activate License', 'activate-license', $slug ); $message = $fs->apply_filters( 'connect-message_on-premium', @@ -186,21 +218,32 @@ class="wrapis_enable_anonymous() $fs->get_plugin_name() ); } else { - $filter = 'connect_message'; - $default_optin_message = $is_gdpr_required ? - fs_text_inline( 'Never miss an important update - opt in to our security & feature updates notifications, educational content, offers, and non-sensitive diagnostic tracking with %4$s.', 'connect-message', $slug) : - fs_text_inline( 'Never miss an important update - opt in to our security and feature updates notifications, and non-sensitive diagnostic tracking with %4$s.', 'connect-message', $slug); + $filter = 'connect_message'; - if ( $fs->is_plugin_update() ) { + if ( ! $fs->is_plugin_update() ) { + $default_optin_message = esc_html( + sprintf( + /* translators: %s: module type (plugin, theme, or add-on) */ + fs_text_inline( 'Opt in to get email notifications for security & feature updates, educational content, and occasional offers, and to share some basic WordPress environment info. This will help us make the %s more compatible with your site and better at doing what you need it to.', 'connect-message', $slug ), + $fs->get_module_label( true ) + ) + ); + } else { // If Freemius was added on a plugin update, set different // opt-in message. - $default_optin_message = $is_gdpr_required ? - fs_text_inline( 'Never miss an important update - opt in to our security & feature updates notifications, educational content, offers, and non-sensitive diagnostic tracking with %4$s. If you skip this, that\'s okay! %1$s will still work just fine.', 'connect-message_on-update', $slug ) : - fs_text_inline( 'Never miss an important update - opt in to our security & feature updates notifications, and non-sensitive diagnostic tracking with %4$s. If you skip this, that\'s okay! %1$s will still work just fine.', 'connect-message_on-update', $slug ); - // If user customized the opt-in message on update, use + /* translators: %s: module type (plugin, theme, or add-on) */ + $default_optin_message = esc_html( sprintf( fs_text_inline( 'We have introduced this opt-in so you never miss an important update and help us make the %s more compatible with your site and better at doing what you need it to.', 'connect-message_on-update_why' ), $fs->get_module_label( true ) ) ); + + $default_optin_message .= '

    ' . esc_html( fs_text_inline( 'Opt in to get email notifications for security & feature updates, educational content, and occasional offers, and to share some basic WordPress environment info.', 'connect-message_on-update', $slug ) ); + + if ( $fs->is_enable_anonymous() ) { + $default_optin_message .= ' ' . esc_html( fs_text_inline( 'If you skip this, that\'s okay! %1$s will still work just fine.', 'connect-message_on-update_skip', $slug ) ); + } + + // If user customized the opt-in message on update, use // that message. Otherwise, fallback to regular opt-in - // custom message if exist. + // custom message if exists. if ( $fs->has_filter( 'connect_message_on_update' ) ) { $filter = 'connect_message_on_update'; } @@ -208,24 +251,19 @@ class="wrapis_enable_anonymous() $message = $fs->apply_filters( $filter, - ($is_network_upgrade_mode ? - '' : - /* translators: %s: name (e.g. Hey John,) */ - $hey_x_text . '
    ' - ) . sprintf( - esc_html( $default_optin_message ), + $default_optin_message, '' . esc_html( $fs->get_plugin_name() ) . '', '' . $current_user->user_login . '', '' . $site_url . '', - $freemius_link + $freemius_activation_terms_html ), $first_name, $fs->get_plugin_name(), $current_user->user_login, '' . $site_url . '', - $freemius_link, - $is_gdpr_required + $freemius_activation_terms_html, + true ); } @@ -266,7 +304,7 @@ class="wrapis_enable_anonymous() * @author Vova Feldman * @since 2.1.2 */ - $fs->do_action( 'connect/after_license_input' ); + $fs->do_action( 'connect/after_license_input', $activation_state ); ?> is_enable_anonymous() echo fs_get_template( 'partials/network-activation.php', $vars ); ?> + + do_action( 'connect/after_message', $activation_state ) ?>

    + do_action( 'connect/before_actions', $activation_state ) ?> + is_enable_anonymous() && ! $is_pending_activation && ( ! $require_license_key || $is_network_upgrade_mode ) ) : ?> @@ -323,9 +365,10 @@ class="button button-secondary" tabindex="2">
    - get_public_key() ) ?> + value="get_unique_affix() . '_activate_existing' ) ?>"> + get_unique_affix() . '_activate_existing' ) ?> +
    @@ -333,9 +376,10 @@ class="button button-secondary" tabindex="2">/action/service/user/install/"> $value ) : ?> - + +
    'dashicons dashicons-admin-users', - 'label' => $fs->get_text_inline( 'Your Profile Overview', 'permissions-profile' ), - 'desc' => $fs->get_text_inline( 'Name and email address', 'permissions-profile_desc' ), - 'priority' => 5, - ); - } - - $permissions['site'] = array( - 'icon-class' => 'dashicons dashicons-admin-settings', - 'tooltip' => ( $require_license_key ? sprintf( $fs->get_text_inline( 'So you can manage and control your license remotely from the User Dashboard.', 'permissions-site_tooltip' ), $fs->get_module_type() ) : '' ), - 'label' => $fs->get_text_inline( 'Your Site Overview', 'permissions-site' ), - 'desc' => $fs->get_text_inline( 'Site URL, WP version, PHP info', 'permissions-site_desc' ), - 'priority' => 10, - ); - - if ( ! $require_license_key ) { - $permissions['notices'] = array( - 'icon-class' => 'dashicons dashicons-testimonial', - 'label' => $fs->get_text_inline( 'Admin Notices', 'permissions-admin-notices' ), - 'desc' => $fs->get_text_inline( 'Updates, announcements, marketing, no spam', 'permissions-newsletter_desc' ), - 'priority' => 13, - ); + // Add newsletter permissions if enabled. + if ( $fs->is_permission_requested( 'newsletter' ) ) { + $permissions[] = $permission_manager->get_newsletter_permission(); } - $permissions['events'] = array( - 'icon-class' => 'dashicons dashicons-admin-' . ( $fs->is_plugin() ? 'plugins' : 'appearance' ), - 'tooltip' => ( $require_license_key ? sprintf( $fs->get_text_inline( 'So you can reuse the license when the %s is no longer active.', 'permissions-events_tooltip' ), $fs->get_module_type() ) : '' ), - 'label' => sprintf( $fs->get_text_inline( 'Current %s Status', 'permissions-events' ), ucfirst( $fs->get_module_type() ) ), - 'desc' => $fs->get_text_inline( 'Active, deactivated, or uninstalled', 'permissions-events_desc' ), - 'priority' => 20, - ); - - // Add newsletter permissions if enabled. - if ( $is_gdpr_required || $fs->is_permission_requested( 'newsletter' ) ) { - $permissions['newsletter'] = array( - 'icon-class' => 'dashicons dashicons-email-alt', - 'label' => $fs->get_text_inline( 'Newsletter', 'permissions-newsletter' ), - 'desc' => $fs->get_text_inline( 'Updates, announcements, marketing, no spam', 'permissions-newsletter_desc' ), - 'priority' => 15, - ); - } - - $permissions['extensions'] = array( - 'icon-class' => 'dashicons dashicons-menu', - 'label' => $fs->get_text_inline( 'Plugins & Themes', 'permissions-extensions' ) . ( $require_license_key ? ' (' . $fs->get_text_inline( 'optional' ) . ')' : '' ), - 'tooltip' => $fs->get_text_inline( 'To help us troubleshoot any potential issues that may arise from other plugin or theme conflicts.', 'permissions-events_tooltip' ), - 'desc' => $fs->get_text_inline( 'Title, slug, version, and is active', 'permissions-extensions_desc' ), - 'priority' => 25, - 'optional' => true, - 'default' => $fs->apply_filters( 'permission_extensions_default', ! $require_license_key ) + $permissions = $permission_manager->get_permissions( + $require_license_key, + $permissions ); - // Allow filtering of the permissions list. - $permissions = $fs->apply_filters( 'permission_list', $permissions ); - - // Sort by priority. - uasort( $permissions, 'fs_sort_by_priority' ); - if ( ! empty( $permissions ) ) : ?>
    - -

    get_module_label( true ), - sprintf('%s', fs_esc_html_inline('diagnostic data', 'send-data')), - 'freemius.com ' . $fs->get_text_inline( 'Freemius is our licensing and software updates engine', 'permissions-extensions_desc' ) . '' - ) ?>

    - - + + ', + sprintf( '%s', esc_html( $fs->get_plugin_title() ) ) + ) ?> + + ', + sprintf( '%s', esc_html( $fs->get_plugin_title() ) ) + ) ?>
      $permission ) : ?> -
    • - - -
      -
      -
      - - -
      - class="fs-tooltip-trigger"> - -

      -
      -
    • - -
    + foreach ( $permissions as $permission ) { + $permission_manager->render_permission( $permission ); + } + ?>
    @@ -463,11 +442,18 @@ class="fs-permission fs-">

    +
    "> * @author Vova Feldman * @since 2.3.2 */ - $fs->do_action( 'connect/after' ); + $fs->do_action( 'connect/after', $activation_state ); if ( $is_optin_dialog ) { ?>
  • @@ -531,13 +517,12 @@ class="fs-permission fs-"> // Reset loading mode. $primaryCta.html(primaryCtaLabel); $primaryCta.prop('disabled', false); - $(document.body).css({'cursor': 'auto'}); - $('.fs-loading').removeClass('fs-loading'); + $( '.fs-loading' ).removeClass( 'fs-loading' ); console.log('resetLoadingMode - Primary button was enabled'); }, setLoadingMode = function () { - $(document.body).css({'cursor': 'wait'}); + $( document.body ).addClass( 'fs-loading' ); }; $('.fs-actions .button').on('click', function () { @@ -601,6 +586,11 @@ class="fs-permission fs-"> updatePrimaryCtaText( actionType ); }); + $sitesListContainer.delegate( 'td:not(:first-child)', 'click', function() { + // If a site row is clicked, trigger a click on the checkbox. + $( this ).parent().find( 'td:first-child input' ).click(); + } ); + $sitesListContainer.delegate( '.action', 'click', function( evt ) { var $this = $( evt.target ); if ( $this.hasClass( 'selected' ) ) { @@ -707,15 +697,25 @@ function updatePrimaryCtaText( actionType ) { var ajaxOptin = ( requireLicenseKey || isNetworkActive ); $form.on('submit', function () { - var $extensionsPermission = $('#fs-permission-extensions .fs-switch'), - isExtensionsTrackingAllowed = ($extensionsPermission.length > 0) ? - $extensionsPermission.hasClass('fs-on') : + var $extensionsPermission = $( '#fs_permission_extensions .fs-switch' ), + isExtensionsTrackingAllowed = ( $extensionsPermission.length > 0 ) ? + $extensionsPermission.hasClass( 'fs-on' ) : + null; + + var $diagnosticPermission = $( '#fs_permission_diagnostic .fs-switch' ), + isDiagnosticTrackingAllowed = ( $diagnosticPermission.length > 0 ) ? + $diagnosticPermission.hasClass( 'fs-on' ) : null; - if (null === isExtensionsTrackingAllowed) { - $('input[name=is_extensions_tracking_allowed]').remove(); + if ( null === isExtensionsTrackingAllowed ) { + $( 'input[name=is_extensions_tracking_allowed]' ).remove(); } else { - $('input[name=is_extensions_tracking_allowed]').val(isExtensionsTrackingAllowed ? 1 : 0); + $( 'input[name=is_extensions_tracking_allowed]' ).val( isExtensionsTrackingAllowed ? 1 : 0 ); + } + + // We are not showing switch to enable/disable diagnostic tracking while activating free version. So, don't remove hidden `is_diagnostic_tracking_allowed` element from DOM and change the value only if switch is available. + if ( null !== isDiagnosticTrackingAllowed ) { + $( 'input[name=is_diagnostic_tracking_allowed]' ).val( isDiagnosticTrackingAllowed ? 1 : 0 ); } /** @@ -772,6 +772,8 @@ function updatePrimaryCtaText( actionType ) { data.is_marketing_allowed = isMarketingAllowed; data.is_extensions_tracking_allowed = isExtensionsTrackingAllowed; + + data.is_diagnostic_tracking_allowed = isDiagnosticTrackingAllowed; } $marketingOptin.removeClass( 'error' ); @@ -794,7 +796,6 @@ function updatePrimaryCtaText( actionType ) { url : $this.find( '.url' ).val(), title : $this.find( '.title' ).val(), language: $this.find( '.language' ).val(), - charset : $this.find( '.charset' ).val(), blog_id : $this.find( '.blog-id' ).find( 'span' ).text() }; @@ -822,7 +823,7 @@ function updatePrimaryCtaText( actionType ) { * @since 1.2.1.5 */ $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : data, success: function (result) { @@ -834,7 +835,7 @@ function updatePrimaryCtaText( actionType ) { resetLoadingMode(); // Show error. - $('.fs-content').prepend('

    ' + (resultObj.error.message ? resultObj.error.message : resultObj.error) + '

    '); + $('.fs-content').prepend('
    ' + (resultObj.error.message ? resultObj.error.message : resultObj.error) + '
    '); } }, error: function () { @@ -858,6 +859,15 @@ function updatePrimaryCtaText( actionType ) { return true; }); + $( '#fs_connect .fs-permissions .fs-switch' ).on( 'click', function () { + $( this ) + .toggleClass( 'fs-on' ) + .toggleClass( 'fs-off' ); + + $( this ).closest( '.fs-permission' ) + .toggleClass( 'fs-disabled' ); + }); + $primaryCta.on('click', function () { console.log('Primary button was clicked'); @@ -874,12 +884,6 @@ function updatePrimaryCtaText( actionType ) { return false; }); - $( '.fs-switch' ).click( function () { - $(this) - .toggleClass( 'fs-on' ) - .toggleClass( 'fs-off' ); - }); - if (requireLicenseKey) { /** * Submit license key on enter. @@ -1005,7 +1009,7 @@ function updatePrimaryCtaText( actionType ) { $primaryCta.html('...'); $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'fetch_is_marketing_required_flag_value' ) ?>', @@ -1035,4 +1039,6 @@ function updatePrimaryCtaText( actionType ) { //endregion })(jQuery); - \ No newline at end of file + + +
  • + + +
    +
    +
    + + +
    + class="fs-tooltip-trigger"> + +

    +
    +
  • \ No newline at end of file diff --git a/freemius/templates/connect/permissions-group.php b/freemius/templates/connect/permissions-group.php new file mode 100644 index 0000000..3abcc43 --- /dev/null +++ b/freemius/templates/connect/permissions-group.php @@ -0,0 +1,72 @@ +get_text_x_inline( 'Opt Out', 'verb', 'opt-out' ); + $opt_in_text = $fs->get_text_x_inline( 'Opt In', 'verb', 'opt-in' ); + + if ( empty( $permission_group[ 'prompt' ] ) ) { + $is_enabled = false; + + foreach ( $permission_group[ 'permissions' ] as $permission ) { + if ( true === $permission[ 'default' ] ) { + // Even if one of the permissions is on, treat as if the entire group is on. + $is_enabled = true; + break; + } + } + } else { + $is_enabled = ( isset( $permission_group['is_enabled'] ) && true === $permission_group['is_enabled'] ); + } +?> +
    +
    +
    + + +
    +

    +
      + render_permission( $permission ); + } + ?> +
    +
    \ No newline at end of file diff --git a/freemius/templates/contact.php b/freemius/templates/contact.php index bba1018..5fdd6e3 100644 --- a/freemius/templates/contact.php +++ b/freemius/templates/contact.php @@ -69,7 +69,7 @@ $query_params = array_merge( $_GET, array_merge( $context_params, array( 'plugin_version' => $fs->get_plugin_version(), 'wp_login_url' => wp_login_url(), - 'site_url' => get_site_url(), + 'site_url' => Freemius::get_unfiltered_site_url(), // 'wp_admin_css' => get_bloginfo('wpurl') . "/wp-admin/load-styles.php?c=1&load=buttons,wp-admin,dashicons", ) ) ); diff --git a/freemius/templates/debug.php b/freemius/templates/debug.php index 4dbf3a3..4626f64 100644 --- a/freemius/templates/debug.php +++ b/freemius/templates/debug.php @@ -16,6 +16,10 @@ $off_text = fs_text_x_inline( 'Off', 'as turned off' ); $on_text = fs_text_x_inline( 'On', 'as turned on' ); + + $has_any_active_clone = false; + + $is_multisite = is_multisite(); ?>

    newest->version ?>

    @@ -35,7 +39,7 @@ .toggleClass( 'fs-on' ) .toggleClass( 'fs-off' ); - $.post( ajaxurl, { + $.post( , { action: 'fs_toggle_debug_mode', // As such we don't need to use `wp_json_encode` method but using it to follow wp.org guideline. _wpnonce : , @@ -79,6 +83,16 @@ + + + +
    + + + +
    + +
    @@ -102,6 +116,15 @@ + + + Resolve Clone(s) + @@ -111,7 +134,7 @@ var optionName = prompt('Please enter the option name:'); if (optionName) { - $.post(ajaxurl, { + $.post(, { action : 'fs_get_db_option', // As such we don't need to use `wp_json_encode` method but using it to follow wp.org guideline. _wpnonce : , @@ -132,7 +155,7 @@ var optionValue = prompt('Please enter the option value:'); if (optionValue) { - $.post(ajaxurl, { + $.post(, { action : 'fs_set_db_option', // As such we don't need to use `wp_json_encode` method but using it to follow wp.org guideline. _wpnonce : , @@ -175,6 +198,10 @@ 'key' => 'WP_FS__DIR', 'val' => WP_FS__DIR, ), + array( + 'key' => 'wp_using_ext_object_cache()', + 'val' => wp_using_ext_object_cache() ? 'true' : 'false', + ), ) ?>
    @@ -229,7 +256,7 @@ WP_FS__MODULE_TYPE_THEME ); ?> - + get_option( $module_type . 's' ), FS_Plugin::get_class_name() ) ?> 0 ) : ?> @@ -245,7 +272,7 @@ - + @@ -268,9 +295,18 @@ } } ?> - id ) : null ?> + id ); + + $active_modules_by_id[ $data->id ] = true; + } + ?> has_api_connectivity() && $fs->is_on() ) { + $has_api_connectivity = $fs->has_api_connectivity(); + + if ( true === $has_api_connectivity && $fs->is_on() ) { echo ' style="background: #E6FFE6; font-weight: bold"'; } else { echo ' style="background: #ffd0d0; font-weight: bold"'; @@ -280,12 +316,14 @@ version ?> title ?> - has_api_connectivity() ) { + >has_api_connectivity() ? + echo esc_html( true === $has_api_connectivity ? fs_text_x_inline( 'Connected', 'as connection was successful' ) : - fs_text_x_inline( 'Blocked', 'as connection blocked' ) + ( false === $has_api_connectivity ? + fs_text_x_inline( 'Blocked', 'as connection blocked' ) : + fs_text_x_inline( 'Unknown', 'API connectivity state is unknown' ) ) ); } ?> is_on() ) { @@ -298,7 +336,7 @@ } ?> file ?> public_key ?> - + 0 ) : ?>

    + $sites ) : ?> - + blog_id : + null; + + if ( is_null( $site_url ) || $is_multisite ) { + $site_url = Freemius::get_unfiltered_site_url( + $blog_id, + true, + true + ); + } + + $is_active_clone = ( $site->is_clone( $site_url ) && isset( $active_modules_by_id[ $site->plugin_id ] ) ); + + if ( $is_active_clone ) { + $has_any_active_clone = true; + } + ?> - id ?> + + id ?> + + + + - blog_id ?> + url ) ?> @@ -484,8 +543,15 @@ * @var FS_User[] $users */ $users = $VARS['users']; + $user_ids_map = array(); $users_with_developer_license_by_id = array(); + if ( is_array( $users ) && ! empty( $users ) ) { + foreach ( $users as $user ) { + $user_ids_map[ $user->id ] = true; + } + } + foreach ( $module_types as $module_type ) { /** * @var FS_Plugin_License[] $licenses @@ -578,7 +644,7 @@ is_block_features ? 'Blocking' : 'Flexible' ?> is_whitelabeled ? 'Whitelabeled' : 'Normal' ?> is_whitelabeled ? + echo ( $license->is_whitelabeled || ! isset( $user_ids_map[ $license->user_id ] ) ) ? $license->get_html_escaped_masked_secret_key() : esc_html( $license->secret_key ); ?> @@ -726,7 +792,7 @@ class="dashicons dashicons-download"> , { action : 'fs_get_debug_log', // As such we don't need to use `wp_json_encode` method but using it to follow wp.org guideline. _wpnonce : , diff --git a/freemius/templates/firewall-issues-js.php b/freemius/templates/firewall-issues-js.php index 6a3f2a5..2b12a15 100644 --- a/freemius/templates/firewall-issues-js.php +++ b/freemius/templates/firewall-issues-js.php @@ -48,8 +48,7 @@ $( this ).css({'cursor': 'wait'}); - // since 2.8 ajaxurl is always defined in the admin header and points to admin-ajax.php - $.post( ajaxurl, data, function( response ) { + $.post( , data, function( response ) { if ( 1 == response ) { // Refresh page on success. location.reload(); diff --git a/freemius/templates/forms/affiliation.php b/freemius/templates/forms/affiliation.php index fe6d694..30a6181 100644 --- a/freemius/templates/forms/affiliation.php +++ b/freemius/templates/forms/affiliation.php @@ -13,8 +13,10 @@ /** * @var array $VARS * @var Freemius $fs + * @var string $plugin_title */ - $fs = freemius( $VARS['id'] ); + $fs = freemius( $VARS['id'] ); + $plugin_title = $VARS['plugin_title']; $slug = $fs->get_slug(); @@ -22,7 +24,6 @@ $affiliate = $fs->get_affiliate(); $affiliate_terms = $fs->get_affiliate_terms(); - $plugin_title = $fs->get_plugin_title(); $module_type = $fs->is_plugin() ? WP_FS__MODULE_TYPE_PLUGIN : WP_FS__MODULE_TYPE_THEME; @@ -45,7 +46,7 @@ $promotion_method_mobile_apps = false; $statistics_information = false; $promotion_method_description = false; - $members_dashboard_login_url = 'https://members.freemius.com/login/'; + $members_dashboard_login_url = 'https://users.freemius.com/login'; $affiliate_application_data = $fs->get_affiliate_application_data(); @@ -71,7 +72,7 @@ $current_user = Freemius::_get_current_wp_user(); $full_name = trim( $current_user->user_firstname . ' ' . $current_user->user_lastname ); $email_address = $current_user->user_email; - $domain = fs_strip_url_protocol( get_site_url() ); + $domain = Freemius::get_unfiltered_site_url( null, true ); } $affiliate_tracking = 30; @@ -86,6 +87,8 @@ $module_id = $fs->get_id(); $affiliate_program_terms_url = "https://freemius.com/plugin/{$module_id}/{$slug}/legal/affiliate-program/"; + + $has_tabs = $fs->_add_tabs_before_content(); ?>
    @@ -365,7 +368,7 @@ } $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'submit_affiliate_application' ) ?>', @@ -499,6 +502,10 @@ function showErrorMessage(message) {
    _add_tabs_after_content(); + } + $params = array( 'page' => 'affiliation', 'module_id' => $module_id, @@ -506,4 +513,3 @@ function showErrorMessage(message) { 'module_version' => $fs->get_plugin_version(), ); fs_require_template( 'powered-by.php', $params ); -?> \ No newline at end of file diff --git a/freemius/templates/forms/data-debug-mode.php b/freemius/templates/forms/data-debug-mode.php index 44cb442..24c3648 100644 --- a/freemius/templates/forms/data-debug-mode.php +++ b/freemius/templates/forms/data-debug-mode.php @@ -140,7 +140,7 @@ function setDeveloperLicenseDebugMode( licenseOrUserKey ) { }; $.ajax( { - url : ajaxurl, + url : , method : 'POST', data : data, beforeSend: function () { diff --git a/freemius/templates/forms/deactivation/form.php b/freemius/templates/forms/deactivation/form.php index 0bdcae0..ec9fcf1 100644 --- a/freemius/templates/forms/deactivation/form.php +++ b/freemius/templates/forms/deactivation/form.php @@ -24,6 +24,7 @@ $anonymous_feedback_checkbox_html = ''; $reasons_list_items_html = ''; + $snooze_select_html = ''; if ( $show_deactivation_feedback_form ) { $reasons = $VARS['reasons']; @@ -64,6 +65,41 @@ fs_esc_html_inline( 'Anonymous feedback', 'anonymous-feedback', $slug ) ); } + + $snooze_periods = array( + array( + 'increment' => fs_text_inline( 'hour', $slug ), + 'quantity' => number_format_i18n(1), + 'value' => 6 * WP_FS__TIME_10_MIN_IN_SEC, + ), + array( + 'increment' => fs_text_inline( 'hours', $slug ), + 'quantity' => number_format_i18n(24), + 'value' => WP_FS__TIME_24_HOURS_IN_SEC, + ), + array( + 'increment' => fs_text_inline( 'days', $slug ), + 'quantity' => number_format_i18n(7), + 'value' => WP_FS__TIME_WEEK_IN_SEC, + ), + array( + 'increment' => fs_text_inline( 'days', $slug ), + 'quantity' => number_format_i18n(30), + 'value' => 30 * WP_FS__TIME_24_HOURS_IN_SEC, + ), + ); + + $snooze_select_html = ''; } // Aliases. @@ -71,6 +107,13 @@ $theme_text = fs_text_inline( 'Theme', 'theme', $slug ); $activate_x_text = fs_text_inline( 'Activate %s', 'activate-x', $slug ); + $submit_deactivate_text = sprintf( + fs_text_inline( 'Submit & %s', 'deactivation-modal-button-submit', $slug ), + $fs->is_plugin() ? + $deactivate_text : + sprintf( $activate_x_text, $theme_text ) + ); + fs_enqueue_local_style( 'fs_dialog_boxes', '/admin/dialog-boxes.css' ); if ( ! empty( $subscription_cancellation_dialog_box_template_params ) ) { @@ -92,6 +135,7 @@ + '

    ' + ' ' @@ -101,6 +145,7 @@ selectedReasonID = false, redirectLink = '', $anonymousFeedback = $modal.find( '.anonymous-feedback-label' ), + $feedbackSnooze = $modal.find( '.feedback-from-snooze-label' ), isAnonymous = , otherReasonID = , dontShareDataReasonID = , @@ -135,7 +180,7 @@ ?> $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'cancel_subscription_or_trial' ) ?>', @@ -230,10 +275,6 @@ function registerEventHandlers() { } ?> $modal.on('input propertychange', '.reason-input input', function () { - if (!isOtherReasonSelected()) { - return; - } - var reason = $(this).val().trim(); /** @@ -241,9 +282,12 @@ function registerEventHandlers() { * to change the message color back to default. */ if (reason.length > 0) { - $('.message').removeClass('error-message'); - enableDeactivateButton(); - } + $('.message').removeClass('error-message'); + } + + toggleDeactivationButtonPrimary( reason.length > 0 ); + + changeDeactivateButtonText(); }); $modal.on('blur', '.reason-input input', function () { @@ -260,8 +304,8 @@ function registerEventHandlers() { */ if (0 === $userReason.val().trim().length) { $('.message').addClass('error-message'); - disableDeactivateButton(); - } + changeDeactivateButtonText(); + } }, 150); }); @@ -276,15 +320,34 @@ function registerEventHandlers() { var _this = $(this); if (_this.hasClass('allow-deactivate')) { - var $radio = $modal.find('input[type="radio"]:checked'); + var + $radio = $modal.find('input[type="radio"]:checked'), + isReasonSelected = (0 < $radio.length), + userReason = ''; + + if ( isReasonSelected ) { + var $selectedReason = $radio.parents('li:first'), + $reasonInput = $selectedReason.find('textarea, input[type="text"]'); - if (0 === $radio.length) { + if ( 0 < $reasonInput.length ) { + userReason = $reasonInput.val().trim(); + } + } + + if ( otherReasonID == selectedReasonID && '' === userReason ) { + // If the 'Other' is selected and a reason is not provided (aka it's empty), treat it as if a reason wasn't selected at all. + isReasonSelected = false; + } + + _parent.find( '.fs-modal-footer .button' ).addClass( 'disabled' ); + + if ( ! isReasonSelected ) { if ( ! deleteThemeUpdateData ) { // If no selected reason, just deactivate the plugin. window.location.href = redirectLink; } else { $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'delete_theme_update_data' ) ?>', @@ -292,8 +355,7 @@ function registerEventHandlers() { module_id: 'get_id() ?>' }, beforeSend: function() { - _parent.find( '.fs-modal-footer .button' ).addClass( 'disabled' ); - _parent.find( '.fs-modal-footer .button-secondary' ).text( 'Processing...' ); + _parent.find( '.fs-modal-footer .button-deactivate' ).text( '...' ); }, complete : function() { window.location.href = redirectLink; @@ -304,28 +366,27 @@ function registerEventHandlers() { return; } - var $selected_reason = $radio.parents('li:first'), - $input = $selected_reason.find('textarea, input[type="text"]'), - userReason = ( 0 !== $input.length ) ? $input.val().trim() : ''; + var snoozePeriod = 0, + shouldSnooze = $feedbackSnooze.find( '.feedback-from-snooze-checkbox' ).is( ':checked' ); - if (isOtherReasonSelected() && ( '' === userReason )) { - return; - } + if ( shouldSnooze && == selectedReasonID ) { + snoozePeriod = parseInt($feedbackSnooze.find('select').val(), 10); + } $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { - action : 'get_ajax_action( 'submit_uninstall_reason' ) ?>', - security : 'get_ajax_security( 'submit_uninstall_reason' ) ?>', - module_id : 'get_id() ?>', - reason_id : $radio.val(), - reason_info : userReason, - is_anonymous: isAnonymousFeedback() + action : 'get_ajax_action( 'submit_uninstall_reason' ) ?>', + security : 'get_ajax_security( 'submit_uninstall_reason' ) ?>', + module_id : 'get_id() ?>', + reason_id : $radio.val(), + reason_info : userReason, + is_anonymous : isAnonymousFeedback(), + snooze_period: snoozePeriod }, beforeSend: function () { - _parent.find('.fs-modal-footer .button').addClass('disabled'); - _parent.find('.fs-modal-footer .button-secondary').text('Processing...'); + _parent.find('.fs-modal-footer .button-deactivate').text('...'); }, complete : function () { // Do not show the dialog box, deactivate the plugin. @@ -365,20 +426,17 @@ function registerEventHandlers() { $modal.find('.reason-input').remove(); $modal.find( '.internal-message' ).hide(); - $modal.find('.button-deactivate').html('is_plugin() ? - $deactivate_text : - sprintf( $activate_x_text, $theme_text ) - ) ) ?>'); - - enableDeactivateButton(); + $modal.find('.button-deactivate').html(''); if ( _parent.hasClass( 'has-internal-message' ) ) { _parent.find( '.internal-message' ).show(); } - if (_parent.hasClass('has-input')) { + if ( ! _parent.hasClass('has-input') ) { + toggleDeactivationButtonPrimary( true ); + } else { + toggleDeactivationButtonPrimary( false ); + var inputType = _parent.data('input-type'), inputPlaceholder = _parent.data('input-placeholder'), reasonInputHtml = '
    ' + ( ( 'textfield' === inputType ) ? '' : '' ) + '
    '; @@ -388,11 +446,60 @@ function registerEventHandlers() { if (isOtherReasonSelected()) { showMessage(''); - disableDeactivateButton(); - } + changeDeactivateButtonText(); + } } + + $anonymousFeedback.toggle( != selectedReasonID ); + $feedbackSnooze.toggle( == selectedReasonID ); + + if ( == selectedReasonID ) { + updateDeactivationButtonOnTrouble(); + } }); + var toggleDeactivationButtonPrimary = function ( isPrimary ) { + if ( isPrimary ) { + $modal.find('.button-deactivate') + .removeClass( 'button-secondary' ) + .addClass( 'button-primary' ); + } else { + $modal.find('.button-deactivate') + .addClass( 'button-secondary' ) + .removeClass( 'button-primary' ); + } + }; + + var snooze = false; + + var updateDeactivationButtonOnTrouble = function () { + if ( snooze ) { + $modal.find('.button-deactivate').html('is_plugin() ? + $deactivate_text : + sprintf( $activate_x_text, $theme_text ) + ) ) ?>'); + } else { + $modal.find('.button-deactivate').html('is_plugin() ? + $deactivate_text : + sprintf( $activate_x_text, $theme_text ) + ) ?>'); + } + }; + + $feedbackSnooze.on( 'click', 'input', function () { + var $spans = $feedbackSnooze.find( 'span' ); + + snooze = ( ! snooze ); + + $( $spans[0] ).toggle(); + $( $spans[1] ).toggle(); + + updateDeactivationButtonOnTrouble(); + }); + // If the user has clicked outside the window, cancel it. $modal.on('click', function (evt) { var $target = $(evt.target); @@ -453,8 +560,6 @@ function closeModal() { function resetModal() { selectedReasonID = false; - enableDeactivateButton(); - // Uncheck all radio buttons. $modal.find('input[type="radio"]').prop('checked', false); @@ -463,8 +568,8 @@ function resetModal() { $modal.find('.message').hide(); - if ( isAnonymous ) { - $anonymousFeedback.find( 'input' ).prop( 'checked', false ); + if ( isAnonymous ) { + $anonymousFeedback.find( 'input' ).prop( 'checked', apply_filters( 'default_to_anonymous_feedback', false ) ? 'true' : 'false' ?> ); // Hide, since by default there is no selected reason. $anonymousFeedback.hide(); @@ -491,13 +596,31 @@ function showMessage(message) { $modal.find('.message').text(message).show(); } - function enableDeactivateButton() { - $modal.find('.button-deactivate').removeClass('disabled'); - } + /** + * @author Xiaheng Chen (@xhchen) + * + * @since 2.4.2 + */ + function changeDeactivateButtonText() { + if ( ! isOtherReasonSelected()) { + return; + } - function disableDeactivateButton() { - $modal.find('.button-deactivate').addClass('disabled'); - } + var + $userReason = $modal.find('.reason-input input'), + $deactivateButton = $modal.find('.button-deactivate'); + + if (0 === $userReason.val().trim().length) { + // If the reason is empty, just change the text to 'Deactivate' (plugin) or 'Activate themeX' (theme). + $deactivateButton.html('is_plugin() ? + $deactivate_text : + sprintf( $activate_x_text, $theme_text ) + ?>'); + } else { + $deactivateButton.html(''); + } + } function showPanel(panelType) { $modal.find( '.fs-modal-panel' ).removeClass( 'active' ); diff --git a/freemius/templates/forms/email-address-update.php b/freemius/templates/forms/email-address-update.php new file mode 100644 index 0000000..49a300f --- /dev/null +++ b/freemius/templates/forms/email-address-update.php @@ -0,0 +1,347 @@ +get_slug(); + + $user = $fs->get_user(); + $current_email_address = $user->email; + + fs_enqueue_local_style( 'fs_dialog_boxes', '/admin/dialog-boxes.css' ); +?> + \ No newline at end of file diff --git a/freemius/templates/forms/license-activation.php b/freemius/templates/forms/license-activation.php index facc12a..2217c36 100644 --- a/freemius/templates/forms/license-activation.php +++ b/freemius/templates/forms/license-activation.php @@ -30,17 +30,10 @@ if ( $fs->is_registered() ) { $activate_button_text = $header_title; } else { - $freemius_site_url = $fs->has_paid_plan() ? - 'https://freemius.com/' : - // Insights platform information. - $fs->get_usage_tracking_terms_url(); - - $freemius_link = 'freemius.com'; - $message_below_input_field = sprintf( - fs_text_inline( 'The %1$s will be periodically sending data to %2$s to check for security and feature updates, and verify the validity of your license.', 'license-sync-disclaimer', $slug ), + fs_text_inline( 'The %1$s will be periodically sending essential license data to %2$s to check for security and feature updates, and verify the validity of your license.', 'license-sync-disclaimer', $slug ), $fs->get_module_label( true ), - $freemius_link + "{$fs->get_plugin_title()}" ); $activate_button_text = fs_text_inline( 'Agree & Activate License', 'agree-activate-license', $slug ); @@ -59,23 +52,51 @@ if ( $is_network_activation ) { $all_sites = Freemius::get_sites(); + $subsite_data_by_install_id = array(); + $install_url_by_install_id = array(); + foreach ( $all_sites as $site ) { $site_details = $fs->get_site_info( $site ); + if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $site_details['blog_id'] ) ) { + continue; + } + $blog_id = Freemius::get_site_blog_id( $site ); $install = $fs->get_install_by_blog_id($blog_id); - if ( is_object( $install ) && FS_Plugin_License::is_valid_id( $install->license_id ) ) { - $site_details['license_id'] = $install->license_id; - } + if ( is_object( $install ) ) { + if ( isset( $subsite_data_by_install_id[ $install->id ] ) ) { + $clone_subsite_data = $subsite_data_by_install_id[ $install->id ]; + $clone_install_url = $install_url_by_install_id[ $install->id ]; + + if ( + /** + * If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match. + * + * @author Leo Fajardo (@leorw) + * @since 2.5.0 + */ + fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) || + fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $site_details['url'] ) ) + ) { + continue; + } + } - $sites_details[] = $site_details; + if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) { + $site_details['license_id'] = $install->license_id; + } + + $subsite_data_by_install_id[ $install->id ] = $site_details; + $install_url_by_install_id[ $install->id ] = $install->url; + } } if ( $is_network_activation ) { $vars = array( 'id' => $fs->get_id(), - 'sites' => $sites_details, + 'sites' => array_values( $subsite_data_by_install_id ), 'require_license_key' => true ); @@ -341,7 +362,7 @@ class="fs-available-license-key" $activateLicenseButton.html( '...' ); $.ajax( { - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'fetch_is_marketing_required_flag_value' ) ?>', @@ -589,7 +610,6 @@ function registerEventHandlers() { url : $this.find( '.url' ).val(), title : $this.find( '.title' ).val(), language: $this.find( '.language' ).val(), - charset : $this.find( '.charset' ).val(), blog_id : $this.find( '.blog-id' ).find( 'span' ).text() }; @@ -607,7 +627,7 @@ function registerEventHandlers() { } $.ajax({ - url: ajaxurl, + url: , method: 'POST', data: data, beforeSend: function () { @@ -867,4 +887,6 @@ function showError( msg ) { } }); })( jQuery ); - \ No newline at end of file + +get_slug(); - $action = $fs->is_tracking_allowed() ? - 'stop_tracking' : - 'allow_tracking'; - $reconnect_url = $fs->get_activation_url( array( 'nonce' => wp_create_nonce( $fs->get_unique_affix() . '_reconnect' ), 'fs_action' => ( $fs->get_unique_affix() . '_reconnect' ), ) ); - $plugin_title = "{$fs->get_plugin()->title}"; - $opt_out_text = fs_text_x_inline( 'Opt Out', 'verb', 'opt-out', $slug ); - $opt_in_text = fs_text_x_inline( 'Opt In', 'verb', 'opt-in', $slug ); - - if ( $fs->is_premium() ) { - $opt_in_message_appreciation = fs_text_inline( 'Connectivity to the licensing engine was successfully re-established. Automatic security & feature updates are now available through the WP Admin Dashboard.', 'premium-opt-in-message-appreciation', $slug ); - - $opt_out_message_subtitle = sprintf( fs_text_inline( 'Warning: Opting out will block automatic updates', 'premium-opt-out-message-appreciation', $slug ), $fs->get_module_type() ); - $opt_out_message_usage_tracking = sprintf( fs_text_inline( 'Ongoing connectivity with the licensing engine is essential for receiving automatic security & feature updates of the paid product. To receive these updates, data like your license key, %1$s version, and WordPress version, is periodically sent to the server to check for updates. By opting out, you understand that your site won\'t receive automatic updates for %2$s from within the WP Admin Dashboard. This can put your site at risk, and we highly recommend to keep this connection active. If you do choose to opt-out, you\'ll need to check for %1$s updates and install them manually.', 'premium-opt-out-message-usage-tracking', $slug ), $fs->get_module_type(), $plugin_title ); - - $primary_cta_label = fs_text_inline( 'I\'d like to keep automatic updates', 'premium-opt-out-cancel', $slug ); - } else { - $opt_in_message_appreciation = sprintf( fs_text_inline( 'We appreciate your help in making the %s better by letting us track some usage data.', 'opt-in-message-appreciation', $slug ), $fs->get_module_type() ); - - $opt_out_message_subtitle = $opt_in_message_appreciation; - $opt_out_message_usage_tracking = sprintf( fs_text_inline( "Usage tracking is done in the name of making %s better. Making a better user experience, prioritizing new features, and more good things. We'd really appreciate if you'll reconsider letting us continue with the tracking.", 'opt-out-message-usage-tracking', $slug ), $plugin_title ); - $primary_cta_label = fs_text_inline( 'On second thought - I want to continue helping', 'opt-out-cancel', $slug ); - } - - $opt_out_message_clicking_opt_out = sprintf( - fs_text_inline( 'By clicking "Opt Out", we will no longer be sending any data from %s to %s.', 'opt-out-message-clicking-opt-out', $slug ), - $plugin_title, - sprintf( - '%s', - 'https://freemius.com', - 'freemius.com' - ) - ); + $plugin_title = "" . esc_html( $fs->get_plugin()->title ) . ""; + $opt_out_text = fs_text_x_inline( 'Opt Out', 'verb', 'opt-out', $slug ); - $admin_notice_params = array( - 'id' => '', - 'slug' => $fs->get_id(), - 'type' => 'success', - 'sticky' => false, - 'plugin' => $fs->get_plugin()->title, - 'message' => $opt_in_message_appreciation - ); - - $admin_notice_html = fs_get_template( 'admin-notice.php', $admin_notice_params ); - - $modal_content_html = " - is_premium() ? ' style="color: red"' : '' ) . ">{$opt_out_message_subtitle} -

    -

    {$opt_out_message_usage_tracking}

    -

    {$opt_out_message_clicking_opt_out}

    - "; + $permission_manager = FS_Permission_Manager::instance( $fs ); fs_enqueue_local_style( 'fs_dialog_boxes', '/admin/dialog-boxes.css' ); + fs_enqueue_local_style( 'fs_optout', '/admin/optout.css' ); fs_enqueue_local_style( 'fs_common', '/admin/common.css' ); + + if ( ! $permission_manager->is_premium_context() ) { + $optional_permissions = array( $permission_manager->get_extensions_permission( false, + false, + true + ) ); + + $permission_groups = array( + array( + 'id' => 'communication', + 'type' => 'required', + 'title' => $fs->get_text_inline( 'Communication', 'communication' ), + 'desc' => '', + 'permissions' => $permission_manager->get_opt_in_required_permissions( true ), + 'is_enabled' => $fs->is_registered(), + 'prompt' => array( + $fs->esc_html_inline( "Sharing your name and email allows us to keep you in the loop about new features and important updates, warn you about security issues before they become public knowledge, and send you special offers.", 'opt-out-message_user' ), + sprintf( + $fs->esc_html_inline( 'By clicking "Opt Out", %s will no longer be able to view your name and email.', + 'opt-out-message-clicking-opt-out' ), + $plugin_title + ), + ), + 'prompt_cancel_label' => $fs->get_text_inline( 'Stay Connected', 'stay-connected' ) + ), + array( + 'id' => 'diagnostic', + 'type' => 'required', + 'title' => $fs->get_text_inline( 'Diagnostic Info', 'diagnostic-info' ), + 'desc' => '', + 'permissions' => $permission_manager->get_opt_in_diagnostic_permissions( true ), + 'is_enabled' => $fs->is_tracking_allowed(), + 'prompt' => array( + sprintf( + $fs->esc_html_inline( 'Sharing diagnostic data helps to provide additional functionality that\'s relevant to your website, avoid WordPress or PHP version incompatibilities that can break the website, and recognize which languages & regions the %s should be translated and tailored to.', + 'opt-out-message-clicking-opt-out' ), + $fs->get_module_type() + ), + sprintf( + $fs->esc_html_inline( 'By clicking "Opt Out", diagnostic data will no longer be sent to %s.', + 'opt-out-message-clicking-opt-out' ), + $plugin_title + ), + ), + 'prompt_cancel_label' => $fs->get_text_inline( 'Keep Sharing', 'keep-sharing' ) + ), + array( + 'id' => 'extensions', + 'type' => 'optional', + 'title' => $fs->get_text_inline( 'Extensions', 'extensions' ), + 'desc' => '', + 'permissions' => $optional_permissions, + ), + ); + } else { + $optional_permissions = $permission_manager->get_license_optional_permissions( false, true ); + + $permission_groups = array( + array( + 'id' => 'essentials', + 'type' => 'required', + 'title' => $fs->esc_html_inline( 'Required', 'required' ), + 'desc' => sprintf( $fs->esc_html_inline( 'For automatic delivery of security & feature updates, and license management & protection, %s needs to:', + 'license-sync-disclaimer' ), + '' . esc_html( $fs->get_plugin_title() ) . '' ), + 'permissions' => $permission_manager->get_license_required_permissions( true ), + 'is_enabled' => $permission_manager->is_essentials_tracking_allowed(), + 'prompt' => array( + sprintf( $fs->esc_html_inline( 'To ensure that security & feature updates are automatically delivered directly to your WordPress Admin Dashboard while protecting your license from unauthorized abuse, %2$s needs to view the website’s homepage URL, %1$s version, SDK version, and whether the %1$s is active.', 'premium-opt-out-message-usage-tracking' ), $fs->get_module_type(), $plugin_title ), + sprintf( $fs->esc_html_inline( 'By opting out from sharing this information with the updates server, you’ll have to check for new %1$s releases and manually download & install them. Not just a hassle, but missing an update can put your site at risk and cause undue compatibility issues, so we highly recommend keeping these essential permissions on.', 'opt-out-message-clicking-opt-out' ), $fs->get_module_type(), $plugin_title ), + ), + 'prompt_cancel_label' => $fs->get_text_inline( 'Keep automatic updates', 'premium-opt-out-cancel' ) + ), + array( + 'id' => 'optional', + 'type' => 'optional', + 'title' => $fs->esc_html_inline( 'Optional', 'optional' ), + 'desc' => sprintf( $fs->esc_html_inline( 'For ongoing compatibility with your website, you can optionally allow %s to:', + 'optional-permissions-disclaimer' ), $plugin_title ), + 'permissions' => $optional_permissions, + ), + ); + } + + $ajax_action = 'toggle_permission_tracking'; + + $form_id = "fs_opt_out_{$fs->get_id()}"; ?> + + +require_permissions_js( false ) ?> + diff --git a/freemius/templates/forms/resend-key.php b/freemius/templates/forms/resend-key.php index f8cafb9..dc723e5 100644 --- a/freemius/templates/forms/resend-key.php +++ b/freemius/templates/forms/resend-key.php @@ -54,7 +54,10 @@ HTML; } - $message_above_input_field = fs_esc_html_inline( "Enter the email address you've used for the upgrade below and we will resend you the license key.", 'ask-for-upgrade-email-address', $slug ); + $message_above_input_field = $fs->is_only_premium() ? + fs_esc_html_inline( "Enter the email address you've used during the purchase and we will resend you the license key.", 'ask-for-upgrade-email-address-premium-only', $slug ) : + fs_esc_html_inline( "Enter the email address you've used for the upgrade below and we will resend you the license key.", 'ask-for-upgrade-email-address', $slug ); + $modal_content_html = <<< HTML

    {$message_above_input_field}

    @@ -142,7 +145,7 @@ function registerEventHandlers() { } $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'resend_license_key' ) ?>', diff --git a/freemius/templates/forms/trial-start.php b/freemius/templates/forms/trial-start.php index b66e727..812363b 100644 --- a/freemius/templates/forms/trial-start.php +++ b/freemius/templates/forms/trial-start.php @@ -80,7 +80,7 @@ function registerEventHandlers() { var $button = $(this); $.ajax({ - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'start_trial' ) ?>', diff --git a/freemius/templates/forms/user-change.php b/freemius/templates/forms/user-change.php index 3571b83..3051b38 100644 --- a/freemius/templates/forms/user-change.php +++ b/freemius/templates/forms/user-change.php @@ -194,7 +194,7 @@ function registerEventHandlers() { disableUserChangeButton(); $.ajax( { - url : ajaxurl, + url : , method : 'POST', data : { action : 'get_ajax_action( 'change_user' ) ?>', diff --git a/freemius/templates/gdpr-optin-js.php b/freemius/templates/gdpr-optin-js.php index 4fdc5e3..80d1eac 100644 --- a/freemius/templates/gdpr-optin-js.php +++ b/freemius/templates/gdpr-optin-js.php @@ -29,7 +29,8 @@ allowMarketing = $this.hasClass( 'allow-marketing' ), cursor = $this.css( 'cursor' ), $products = $gdprOptinNotice.find( 'span[data-plugin-id]' ), - pluginIDs = []; + pluginIDs = [], + ajaxUrl = ; if ( $products.length > 0 ) { $products.each(function() { @@ -38,7 +39,7 @@ } $.ajax({ - url : ajaxurl + '?' + $.param({ + url : ajaxUrl + (ajaxUrl.includes('?') ? '&' : '?') + $.param({ action : 'get_ajax_action( 'gdpr_optin_action' ) ?>', security : 'get_ajax_security( 'gdpr_optin_action' ) ?>', module_id: 'get_id() ?>' diff --git a/freemius/templates/js/permissions.php b/freemius/templates/js/permissions.php new file mode 100644 index 0000000..dbc7dfd --- /dev/null +++ b/freemius/templates/js/permissions.php @@ -0,0 +1,546 @@ + + \ No newline at end of file diff --git a/freemius/templates/partials/network-activation.php b/freemius/templates/partials/network-activation.php index 12f152f..22b2270 100644 --- a/freemius/templates/partials/network-activation.php +++ b/freemius/templates/partials/network-activation.php @@ -58,21 +58,21 @@
    - + > - + diff --git a/freemius/templates/plugin-icon.php b/freemius/templates/plugin-icon.php index ab0fb54..4422c90 100644 --- a/freemius/templates/plugin-icon.php +++ b/freemius/templates/plugin-icon.php @@ -14,7 +14,9 @@ * @var array $VARS */ $fs = freemius( $VARS['id'] ); + + $size = isset( $VARS['size'] ) ? $VARS['size'] : 80; ?>
    - +
    \ No newline at end of file diff --git a/freemius/templates/plugin-info/description.php b/freemius/templates/plugin-info/description.php index 26bc67b..cbef32b 100644 --- a/freemius/templates/plugin-info/description.php +++ b/freemius/templates/plugin-info/description.php @@ -52,15 +52,10 @@ info->screenshots ) ) : ?> info->screenshots ?>
    -

    slug ) ?>

    +

    slug ) ?>

      $url ) : ?> -
    • +
      +
      +
      + +
      +
      + +
      +
      +
      + $func(); + + } + elseif( is_string( $metabox_callback ) && function_exists( $metabox_callback ) ) { + + $metabox_callback(); + + } + + ?> +
      +
      + '; + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $field; + } + else { + return $field; + } + + } +endif; + +if ( ! function_exists( 'mycred_create_select_field' ) ) : + function mycred_create_select_field( $childs, $selected = array(), $atts = array(), $echo = true ) { + + if ( empty( $atts['type'] ) || in_array( $atts['type'], array( 'checkbox', 'radio', 'button', 'submit' ) ) ) + $atts['type'] = 'text'; + + if ( empty( $atts['class'] ) ) + $atts['class'] = 'mycred-ui-form'; + else + $atts['class'] = 'mycred-ui-form ' . $atts['class']; + + $field = ''; + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $field; + } + else { + return $field; + } + + } +endif; + +if ( ! function_exists( 'mycred_get_attribute_html' ) ) : + function mycred_get_attribute_html( $data ) { + + $attributes = array(); + + if ( ! empty( $data ) && is_array( $data ) ) { + + foreach ( $data as $attribute => $attribute_value ) { + + if ( ! empty( $attribute_value ) ) + $attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; + + } + + } + + return implode( ' ', $attributes ); + + } +endif; + +if ( ! function_exists( 'mycred_selected' ) ) : + function mycred_selected( $selected, $current, $echo = true ) { + + $result = ''; + + if ( + ( is_array( $selected ) && in_array( $current, $selected ) ) || + ( ! is_array( $selected ) && (string) $current === (string) $selected ) + ) + $result = " selected='selected'"; + + if ( $echo ) { + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $result; + } + else { + return $result; + } + + } +endif; + +/** + * Cretae Evidence page + * @since 2.5 + * @version 1.0 + */ +if ( ! function_exists( 'mycred_get_evidence_page_id' ) ) : + function mycred_get_evidence_page_id() { + + $evidencePageId = 0; + + $hooks = mycred_get_option( 'mycred_pref_core' ); + + $badges = array_key_exists( 'open_badge', $hooks ) ? $hooks['open_badge'] : array(); + + //If Open badge enabled + if ( isset( $badges['is_enabled'] ) && $badges['is_enabled'] == '1' ) { + + $canCreatePage = true; + + $evidence_page_refrence = mycred_get_option( 'open_badge_evidence_page', 0 ); + + if ( ! empty( $badges['evidence_page'] ) || ! empty( $evidence_page_refrence ) ) { + + $pageId = intval( $evidence_page_refrence ); + + if ( ! empty( $badges['evidence_page'] ) ) { + + $pageId = intval( $badges['evidence_page'] ); + + } + + if ( get_post_status( $pageId ) == 'publish' ) { + + $canCreatePage = false; + $evidencePageId = $pageId; + + } + + } + + if ( $canCreatePage ) { + + $postData = array( + 'post_content' => '[' . MYCRED_SLUG . '_badge_evidence]', + 'post_title' => 'Badge Evidence', + 'post_status' => 'publish', + 'post_type' => 'page', + 'comment_status' => 'closed', + 'post_name' => 'Badge Evidence' + ); + + $pageId = wp_insert_post( $postData ); + + $evidencePageId = intval( $pageId ); + + mycred_update_option( 'open_badge_evidence_page', $evidencePageId ); + + mycred_set_badge_evidence_page( $evidencePageId ); + + } + + } + + return $evidencePageId; + + } +endif; + +/** + * Set Evidence page + * @since 2.5 + * @version 1.0 + */ +if ( ! function_exists( 'mycred_set_badge_evidence_page' ) ) : + function mycred_set_badge_evidence_page( $page_id ) { + + $settings = mycred_get_option( 'mycred_pref_core' ); + + if ( isset( $settings[ 'open_badge' ] ) ) { + + $settings['open_badge'][ 'evidence_page' ] = intval( $page_id ); + + mycred_update_option( 'mycred_pref_core', $settings ); + + } + + } +endif; + +/** + * Returns Badge main image with share icons. + * @since 2.2 + * @version 1.0 + */ +if ( ! function_exists( 'mycred_badge_plus_show_main_image_with_social_icons' ) ) : + function mycred_badge_show_main_image_with_social_icons( $earned_image_url, $user_has_badge ) { + + $content = ''; + + $image_url = $earned_image_url; + + if ( ! empty( $image_url ) ) { + + $content .= '
      '; + + $content .= 'Badge Image'; + $mycred = mycred(); + + //If user has earned badge, show user sharing badge option + if( + $user_has_badge && + ! empty( $mycred->core["br_social_share"]["enable_open_badge_ss"] ) + ) { + + $facebook_url = "http://www.facebook.com/sharer.php?u=".get_permalink()."&p[images][0]=$image_url"; + $twitter_url = "https://twitter.com/share?url=".get_permalink().""; + $linkedin_url = "http://www.linkedin.com/shareArticle?url=".get_permalink().""; + $pinterest_url = "https://pinterest.com/pin/create/bookmarklet/?media=$image_url&url=".get_permalink().""; + + $content .= mycred_br_get_social_icons( $facebook_url, $twitter_url, $linkedin_url, $pinterest_url ); + + } + + $content .= '
      '; + + } + + return apply_filters( 'mycred_badge_show_main_image_with_social_icons', $content, $mycred ); + + } +endif; + +/** + * override old open badge setting to new open badge setting menu + * @since 2.5 + * @version 1.0 + */ +if ( ! function_exists( 'mycred_override_open_badge' ) ) : + function mycred_override_open_badge() { + + $settings = mycred_get_option( 'mycred_pref_core' ); + + if ( isset( $settings[ 'badges' ]['open_badge'] ) ) { + + $settings[ 'open_badge' ] = array(); + $settings[ 'open_badge' ]['is_enabled'] = $settings['badges']['open_badge']; + $settings[ 'open_badge' ]['evidence_page'] = isset( $settings['badges']['open_badge_evidence_page'] ) ? $settings['badges']['open_badge_evidence_page'] : 0; + + unset( $settings['badges']['open_badge'] ); + unset( $settings['badges']['open_badge_evidence_page'] ); + + mycred_update_option( 'mycred_pref_core', $settings ); + + } + + } +endif; \ No newline at end of file diff --git a/includes/mycred-open-badge-settings.php b/includes/mycred-open-badge-settings.php new file mode 100644 index 0000000..90b7406 --- /dev/null +++ b/includes/mycred-open-badge-settings.php @@ -0,0 +1,124 @@ +core, 'open_badge' ) ? $mycred->core->open_badge : array(); ?> + +

      + get_all_users(); + $users = array(); $users_args = array( 'name' => $args['users']['name'], diff --git a/includes/shortcodes/mycred-badge-evidence-page.php b/includes/shortcodes/mycred-badge-evidence-page.php new file mode 100644 index 0000000..04b87ec --- /dev/null +++ b/includes/shortcodes/mycred-badge-evidence-page.php @@ -0,0 +1,62 @@ +Evidence not found
    '; + + if ( isset( $_GET['uid'] ) && isset( $_GET['bid'] ) ) { + + $user_id = intval( $_GET['uid'] ); + $badge_id = intval( $_GET['bid'] ); + + $user_info = get_userdata( $user_id ); + $post = get_post( $badge_id ); + + $issued_on = ''; + if( $post->post_type == 'mycred_badge' ){ + $badge = mycred_get_badge( $badge_id ); + $issued_on = mycred_get_user_meta( $user_id, MYCRED_BADGE_KEY . $badge_id, '_issued_on', true ); + + } + + if ( $post->post_type == 'mycred_badge_plus' ) { + $badge = mycred_badge_plus_object( $badge_id ); + $issued_on = end( mycred_get_user_meta( $user_id, 'mycred_badge_plus_ids', '', true )[$badge_id] ); + } + + if ( $user_info && $badge->open_badge ) { + + $content = '
    +
    + +
    +
    +

    ' . $badge->title . '

    +
    +

    Name: '. $user_info->display_name .'

    +

    Email: ' . $user_info->user_email . '

    +

    Issued On: ' . date( 'Y-m-d\TH:i:sP', $issued_on ) . '

    +

    Verified

    +
    +
    +
    +
    '; + + } + + + } + + return $content; + } +endif; + +add_shortcode( MYCRED_SLUG . '_badge_evidence', 'mycred_render_badge_evidence' ); \ No newline at end of file diff --git a/includes/shortcodes/mycred_exchange.php b/includes/shortcodes/mycred_exchange.php index 33e9084..0a65053 100644 --- a/includes/shortcodes/mycred_exchange.php +++ b/includes/shortcodes/mycred_exchange.php @@ -76,7 +76,7 @@ function mycred_render_shortcode_exchange( $atts, $content = '' ) {

    %s %s', 'mycred' ), esc_html( $mycred_from->singular() ), esc_html( $rate ), ( ( $rate == 1 ) ? esc_html( $mycred_to->singular() ) : esc_html( $mycred_to->plural() ) ) ); ?>

    -
    +
    diff --git a/membership/mycred-connect-membership.php b/membership/mycred-connect-membership.php index 366112b..9634895 100644 --- a/membership/mycred-connect-membership.php +++ b/membership/mycred-connect-membership.php @@ -134,7 +134,7 @@ public function mycred_support_callback() {

    Suggestion:

    -

    If you have suggestions for myCred and their addons, feel free to add them here.

    +

    If you have suggestions for myCred and their addons, feel free to add them here.


    Free add-ons

    @@ -270,26 +270,41 @@ public function mycred_membership_callback() { $membership_key = get_option( 'mycred_membership_key' ); if( !isset( $membership_key ) && !empty( $membership_key ) ) $membership_key = ''; + global $license_valid_check; + global $license_empty_check; ?>
    -
    +
    Please Enter a License Key.
    '; + } elseif ( isset( $license_valid_check ) && true == $license_valid_check ) { + + echo '
    The License Key is Valid.
    '; + } elseif ( isset( $license_valid_check ) && false == $license_valid_check ) { + + echo '
    The License Key is Invalid.
    '; + } + ?> +
    '; } else { // if membership is not active in current site and the membership key is entered echo ''; - } - - + } ?> - +
    .. - +