@@ -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() {
- ' . esc_js( esc_attr($item_label ) ) . '
- ' . esc_js( esc_attr($amount_label ) ) . '
+ ' . esc_js( esc_attr( $item_label ) ) . '
+ ' . esc_js( esc_attr( $amount_label ) ) . '
@@ -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() {
?>
-
-
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(
- '
%s %s %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(
- '
%s %s %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(
- '
%s %s %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(
- '
%s %s %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}
" .
+ '
' .
+ '' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' .
+ $result->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(
- '
%s ➜ ',
- $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(
'
%s ',
- 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, '