Fix File
•
/
home
/
sportsfe...
/
httpdocs
/
clone
/
wp-conte...
/
plugins
/
content-...
/
inc
/
freemius
/
includes
•
File:
class-freemius.php
•
Content:
* @param number|bool $module_id * @param FS_Plugin_License[] $licenses */ private function _store_licenses( $store = true, $module_id = false, $licenses = array() ) { $this->_logger->entrance(); $all_licenses = self::get_all_licenses(); if ( ! FS_Plugin::is_valid_id( $module_id ) ) { $module_id = $this->_module_id; $user_licenses = is_array( $this->_licenses ) ? $this->_licenses : array(); if ( empty( $user_licenses ) ) { // If the context user doesn't have any license, don't update the licenses collection. return; } $new_user_licenses_map = array(); foreach ( $user_licenses as $user_license ) { $new_user_licenses_map[ $user_license->id ] = $user_license; } self::store_user_id_license_ids_map( array_keys( $new_user_licenses_map ), $this->_module_id, $this->_user->id ); // Update user licenses. $licenses_to_update_count = count( $new_user_licenses_map ); foreach ( $all_licenses[ $module_id ] as $key => $license ) { if ( 0 === $licenses_to_update_count ) { break; } if ( isset( $new_user_licenses_map[ $license->id ] ) ) { // Update license. $all_licenses[ $module_id ][ $key ] = $new_user_licenses_map[ $license->id ]; unset( $new_user_licenses_map[ $license->id ] ); $licenses_to_update_count --; } } if ( ! empty( $new_user_licenses_map ) ) { // Add new licenses. $all_licenses[ $module_id ] = array_merge( array_values( $new_user_licenses_map ), $all_licenses[ $module_id ] ); } $licenses = $all_licenses[ $module_id ]; } if ( ! isset( $all_licenses[ $module_id ] ) ) { $all_licenses[ $module_id ] = array(); } $all_licenses[ $module_id ] = $licenses; self::$_accounts->set_option( 'all_licenses', $all_licenses, $store ); } /** * Update user information. * * @author Vova Feldman (@svovaf) * @since 1.0.1 * * @param bool $store Flush to Database if true. */ private function _store_user( $store = true ) { $this->_logger->entrance(); if ( empty( $this->_user->id ) ) { $this->_logger->error( "Empty user ID, can't store user." ); return; } $users = self::get_all_users(); $users[ $this->_user->id ] = $this->_user; self::$_accounts->set_option( 'users', $users, $store ); } /** * Update new updates information. * * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @param FS_Plugin_Tag|null $update * @param bool $store Flush to Database if true. * @param bool|number $plugin_id */ private function _store_update( $update, $store = true, $plugin_id = false ) { $this->_logger->entrance(); if ( $update instanceof FS_Plugin_Tag ) { $update->updated = time(); } if ( ! is_numeric( $plugin_id ) ) { $plugin_id = $this->_plugin->id; } $updates = self::get_all_updates(); $updates[ $plugin_id ] = $update; self::$_accounts->set_option( 'updates', $updates, $store ); } /** * Update new updates information. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @param FS_Plugin[] $plugin_addons * @param bool $store Flush to Database if true. */ private function _store_addons( $plugin_addons, $store = true ) { $this->_logger->entrance(); $addons = self::get_all_addons(); $addons[ $this->_plugin->id ] = $plugin_addons; self::$_accounts->set_option( 'addons', $addons, $store ); } /** * Delete plugin's associated add-ons. * * @author Vova Feldman (@svovaf) * @since 1.0.8 * * @param bool $store * * @return bool */ private function _delete_account_addons( $store = true ) { $all_addons = self::get_all_account_addons(); if ( ! isset( $all_addons[ $this->_plugin->id ] ) ) { return false; } unset( $all_addons[ $this->_plugin->id ] ); self::$_accounts->set_option( 'account_addons', $all_addons, $store ); return true; } /** * Update account add-ons list. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @param FS_Plugin[] $addons * @param bool $store Flush to Database if true. */ private function _store_account_addons( $addons, $store = true ) { $this->_logger->entrance(); $all_addons = self::get_all_account_addons(); $all_addons[ $this->_plugin->id ] = $addons; self::$_accounts->set_option( 'account_addons', $all_addons, $store ); } /** * Purges the cache for the valid user licenses API call so that when the `Account` or `Add-Ons` page is loaded, * the valid user licenses will be fetched again and the account add-ons may be updated. * * @author Leo Fajardo (@leorw) * @since 2.2.4 */ private function purge_valid_user_licenses_cache() { if ( ! $this->is_registered() ) { return; } $this->get_api_user_scope()->purge_cache( $this->get_valid_user_licenses_endpoint() ); } /** * @author Leo Fajardo (@leorw) * @since 2.3.0 * * @param array $all_licenses * @param number|null $site_license_id * @param bool $include_parent_licenses * * @return array */ private function get_foreign_licenses_info( $all_licenses, $site_license_id = null, $include_parent_licenses = false ) { $foreign_licenses = array( 'ids' => array(), 'license_keys' => array() ); $parent_license_ids_map = array(); foreach ( $all_licenses as $license ) { if ( $license->user_id == $this->_user->id || $license->id == $site_license_id ) { continue; } $foreign_licenses['ids'][] = $license->id; $foreign_licenses['license_keys'][] = $license->secret_key; if ( $include_parent_licenses && is_object( $this->_license ) && FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) && ! isset( $parent_license_ids_map[ $this->_license->parent_license_id ] ) ) { /** * Include the parent license's info only if it has not been included before since child licenses * can have the same parent license. */ $foreign_licenses['ids'][] = $this->_license->parent_license_id; $foreign_licenses['license_keys'][] = $license->secret_key; $parent_license_ids_map[ $this->_license->parent_license_id ] = true; } } if ( empty( $foreign_licenses['ids'] ) ) { $foreign_licenses = array(); } return $foreign_licenses; } /** * @author Leo Fajardo (@leorw) * @since 2.3.0 * * @return string */ private function get_valid_user_licenses_endpoint() { $user_licenses_endpoint = '/licenses.json?type=active' . ( FS_Plugin::is_valid_id( $this->get_bundle_id() ) ? '&is_enriched=true' : '' ); $foreign_licenses = $this->get_foreign_licenses_info( self::get_all_licenses( $this->_module_id ), null, true ); if ( ! empty ( $foreign_licenses ) ) { $foreign_licenses = array( // Prefix with `+` to tell the server to include foreign licenses in the licenses collection. 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ), 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) ) ); $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint ); } return $user_licenses_endpoint; } /** * Fetches active licenses that are enriched with product type if there's a context `bundle_id` and bundle * licenses enriched with product IDs if there are any. From the licenses, the `get_updated_account_addons` * method filters out non–add-on product IDs and stores the add-on IDs. * * @author Leo Fajardo (@leorw) * @since 2.2.4 * * @return stdClass[] array */ private function fetch_valid_user_licenses() { $this->_logger->entrance(); $result = $this->get_api_user_scope()->get( $this->get_valid_user_licenses_endpoint() ); if ( ! $this->is_api_result_object( $result, 'licenses' ) || ! is_array( $result->licenses ) ) { return array(); } return $result->licenses; } /** * @author Leo Fajardo (@leorw) * @since 2.2.4 * * @return number[] Account add-on IDs. */ function get_updated_account_addons() { $addons = $this->get_addons(); if ( empty( $addons ) ) { return array(); } $account_addons = $this->get_account_addons(); if ( ! is_array( $account_addons ) ) { $account_addons = array(); } $user_licenses = $this->is_registered() ? $this->fetch_valid_user_licenses() : array(); if ( empty( $user_licenses ) ) { return $account_addons; } $addon_ids = array(); foreach ( $addons as $addon ) { $addon_ids[] = $addon->id; } $license_product_ids = array(); foreach ( $user_licenses as $license ) { if ( isset( $license->plugin_type ) && 'bundle' === $license->plugin_type ) { $license_product_ids = array_merge( $license_product_ids, $license->products ); } else { $license_product_ids[] = $license->plugin_id; } } // Filter out non–add-on IDs. $new_account_addons = array_intersect( $addon_ids, $license_product_ids ); if ( count( $new_account_addons ) !== count( $account_addons ) ) { $this->_store_account_addons( array_unique( $new_account_addons ) ); } return $new_account_addons; } /** * Store account params in the Database. * * @author Vova Feldman (@svovaf) * @since 1.0.1 * * @param null|int $blog_id Since 2.0.0 */ private function _store_account( $blog_id = null ) { $this->_logger->entrance(); $this->_store_site( false, $blog_id ); $this->_store_user( false ); $this->_store_plans( false ); $this->_store_licenses( false ); self::$_accounts->store( $blog_id ); } /** * Sync user's information. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * @uses FS_Api */ private function _handle_account_user_sync() { $this->_logger->entrance(); $api = $this->get_api_user_scope(); // Get user's information. $user = $api->get( '/', true ); if ( isset( $user->id ) ) { $this->_user->first = $user->first; $this->_user->last = $user->last; $this->_user->email = $user->email; $is_menu_item_account_visible = $this->is_submenu_item_visible( 'account' ); if ( $user->is_verified && ( ! isset( $this->_user->is_verified ) || false === $this->_user->is_verified ) ) { $this->_user->is_verified = true; $this->do_action( 'account_email_verified', $user->email ); $this->_admin_notices->add( $this->get_text_inline( 'Your email has been successfully verified - you are AWESOME!', 'email-verified-message' ), $this->get_text_x_inline( 'Right on', 'a positive response', 'right-on' ) . '!', 'success', // Make admin sticky if account menu item is invisible, // since the page will be auto redirected to the plugin's // main settings page, and the non-sticky message // will disappear. ! $is_menu_item_account_visible, 'email_verified' ); } // Flush user details to DB. $this->_store_user(); $this->do_action( 'after_account_user_sync', $user ); /** * If account menu item is hidden, redirect to plugin's main settings page. * * @author Vova Feldman (@svovaf) * @since 1.1.6 * * @link https://github.com/Freemius/wordpress-sdk/issues/6 */ if ( ! $is_menu_item_account_visible ) { fs_redirect( $this->_get_admin_page_url() ); } } } /** * @author Vova Feldman (@svovaf) * @since 1.0.9 * @uses FS_Api * * @param number|bool $license_id * * @return FS_Subscription|object|bool */ private function _fetch_site_license_subscription( $license_id = false ) { $this->_logger->entrance(); $api = $this->get_api_site_scope(); if ( ! is_numeric( $license_id ) ) { $license_id = FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ) ? $this->_license->parent_license_id : $this->_license->id; } $result = $api->get( "/licenses/{$license_id}/subscriptions.json", true ); return ! isset( $result->error ) ? ( ( is_array( $result->subscriptions ) && 0 < count( $result->subscriptions ) ) ? new FS_Subscription( $result->subscriptions[0] ) : false ) : $result; } /** * @author Vova Feldman (@svovaf) * @since 1.0.4 * @uses FS_Api * * @param number|bool $plan_id * * @return FS_Plugin_Plan|object */ private function _fetch_site_plan( $plan_id = false ) { $this->_logger->entrance(); $api = $this->get_api_site_scope(); if ( ! is_numeric( $plan_id ) ) { $plan_id = $this->_site->plan_id; } $plan = $api->get( "/plans/{$plan_id}.json", true ); return ! isset( $plan->error ) ? new FS_Plugin_Plan( $plan ) : $plan; } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * @uses FS_Api * * @return FS_Plugin_Plan[]|object */ private function _fetch_plugin_plans() { $this->_logger->entrance(); $api = $this->get_current_or_network_user_api_scope(); /** * @since 1.2.3 When running in DEV mode, retrieve pending plans as well. */ $result = $api->get( $this->add_show_pending( "/plugins/{$this->_module_id}/plans.json" ), true ); if ( $this->is_api_result_object( $result, 'plans' ) && is_array( $result->plans ) ) { for ( $i = 0, $len = count( $result->plans ); $i < $len; $i ++ ) { $result->plans[ $i ] = new FS_Plugin_Plan( $result->plans[ $i ] ); } $result = $result->plans; } return $result; } /** * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @param number $plan_id * * @return \FS_Plugin_Plan|object */ private function fetch_plan_by_id( $plan_id ) { $this->_logger->entrance(); $api = $this->get_current_or_network_user_api_scope(); $result = $api->get( "/plugins/{$this->_module_id}/plans/{$plan_id}.json", true ); return $this->is_api_result_entity( $result ) ? new FS_Plugin_Plan( $result ) : $result; } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * @uses FS_Api * * @param number|bool $plugin_id * @param number|bool $site_license_id * @param array $foreign_licenses @since 2.0.0. This is used by network-activated plugins. * @param number|null $blog_id * * @return FS_Plugin_License[]|object */ private function _fetch_licenses( $plugin_id = false, $site_license_id = false, $foreign_licenses = array(), $blog_id = null ) { $this->_logger->entrance(); $api = $this->get_api_user_scope(); if ( ! is_numeric( $plugin_id ) ) { $plugin_id = $this->_plugin->id; } $user_licenses_endpoint = "/plugins/{$plugin_id}/licenses.json?is_enriched=true"; if ( ! empty ( $foreign_licenses ) ) { $foreign_licenses = array( // Prefix with `+` to tell the server to include foreign licenses in the licenses collection. 'ids' => ( urlencode( '+' ) . implode( ',', $foreign_licenses['ids'] ) ), 'license_keys' => implode( ',', array_map( 'urlencode', $foreign_licenses['license_keys'] ) ) ); $user_licenses_endpoint = add_query_arg( $foreign_licenses, $user_licenses_endpoint ); } $result = $api->get( $user_licenses_endpoint, true ); $is_site_license_synced = false; $api_errors = array(); if ( $this->is_api_result_object( $result, 'licenses' ) && is_array( $result->licenses ) ) { for ( $i = 0, $len = count( $result->licenses ); $i < $len; $i ++ ) { $result->licenses[ $i ] = new FS_Plugin_License( $result->licenses[ $i ] ); if ( ( ! $is_site_license_synced ) && is_numeric( $site_license_id ) ) { $is_site_license_synced = ( $site_license_id == $result->licenses[ $i ]->id ); } } $result = $result->licenses; } else { $api_errors[] = $result; $result = array(); } if ( ! $is_site_license_synced ) { if ( ! is_null( $blog_id ) ) { /** * If blog ID is not null, the request is for syncing of the license of a single site via the * network-level "Account" page. * * @author Leo Fajardo (@leorw) */ $this->switch_to_blog( $blog_id ); } $api = $this->get_api_site_scope(); if ( is_numeric( $site_license_id ) ) { // Try to retrieve a foreign license that is linked to the install. $api_result = $api->call( '/licenses.json?is_enriched=true' ); if ( $this->is_api_result_object( $api_result, 'licenses' ) && is_array( $api_result->licenses ) ) { $licenses = $api_result->licenses; if ( ! empty( $licenses ) ) { $result[] = new FS_Plugin_License( $licenses[0] ); } } else { $api_errors[] = $api_result; } } else if ( is_object( $this->_license ) && /** * Sync only if the license belongs to the context plugin. `$plugin_id` can be an add-on ID while * the FS instance that does the syncing is the parent FS instance. * * @author Leo Fajardo (@leorw) * @since 2.3.0 */ $this->_license->plugin_id == $plugin_id ) { $is_license_in_result = false; if ( ! empty( $result ) ) { foreach ( $result as $license ) { if ( $license->id == $this->_license->id ) { $is_license_in_result = true; break; } } } if ( ! $is_license_in_result ) { // Fetch foreign license by ID and license key. $license = $api->get( "/licenses/{$this->_license->id}.json?license_key=" . urlencode( $this->_license->secret_key ) . '&is_enriched=true' ); if ( $this->is_api_result_entity( $license ) ) { $result[] = new FS_Plugin_License( $license ); } else { $api_errors[] = $license; } } } if ( ! is_null( $blog_id ) ) { $this->switch_to_blog( $this->_storage->network_install_blog_id ); } } if ( is_array( $result ) && 0 < count( $result ) ) { // If found at least one license, return license collection even if there are errors. return $result; } if ( ! empty( $api_errors ) ) { // If found any errors and no licenses, return first error. return $api_errors[0]; } // Fallback to empty licenses list. return $result; } /** * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @param number $license_id * @param string $license_key * * @return \FS_Plugin_License|object */ private function fetch_license_by_key( $license_id, $license_key ) { $this->_logger->entrance(); $api = $this->get_current_or_network_user_api_scope(); $result = $api->get( "/licenses/{$license_id}.json?license_key=" . urlencode( $license_key ) ); return $this->is_api_result_entity( $result ) ? new FS_Plugin_License( $result ) : $result; } /** * @author Vova Feldman (@svovaf) * @since 1.2.0 * @uses FS_Api * * @param number|bool $plugin_id * @param bool $flush * * @return FS_Payment[]|object */ function _fetch_payments( $plugin_id = false, $flush = false ) { $this->_logger->entrance(); $api = $this->get_api_user_scope(); if ( ! is_numeric( $plugin_id ) ) { $plugin_id = $this->_plugin->id; } $include_bundles = ( is_object( $this->_plugin ) && FS_Plugin::is_valid_id( $this->_plugin->bundle_id ) ); $result = $api->get( "/plugins/{$plugin_id}/payments.json?include_addons=true" . ($include_bundles ? '&include_bundles=true' : ''), $flush ); if ( ! isset( $result->error ) ) { for ( $i = 0, $len = count( $result->payments ); $i < $len; $i ++ ) { $result->payments[ $i ] = new FS_Payment( $result->payments[ $i ] ); } $result = $result->payments; } return $result; } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * @uses FS_Api * * @param bool $flush * * @return \FS_Billing|mixed */ function _fetch_billing( $flush = false ) { require_once WP_FS__DIR_INCLUDES . '/entities/class-fs-billing.php'; $billing = $this->get_api_user_scope()->get( 'billing.json', $flush ); if ( $this->is_api_result_entity( $billing ) ) { $billing = new FS_Billing( $billing ); } return $billing; } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * * @param FS_Plugin_License[] $licenses * @param number $module_id */ private function _update_licenses( $licenses, $module_id ) { $this->_logger->entrance(); if ( is_array( $licenses ) ) { for ( $i = 0, $len = count( $licenses ); $i < $len; $i ++ ) { $licenses[ $i ]->updated = time(); } } $this->_store_licenses( true, $module_id, $licenses ); } /** * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @param bool|number $plugin_id * @param bool $flush Since 1.1.7.3 * @param int $expiration Since 1.2.2.7 * @param bool|string $newer_than Since 2.2.1 * * @return object|false New plugin tag info if exist. */ private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) { $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than ); if ( ! is_object( $latest_tag ) ) { return false; } $plugin_version = $this->get_plugin_version(); // Check if version is actually newer. $has_new_version = // If it's an non-installed add-on then always return latest. ( $this->_is_addon_id( $plugin_id ) && ! $this->is_addon_activated( $plugin_id ) ) || // Compare versions. version_compare( $plugin_version, $latest_tag->version, '<' ); $this->_logger->departure( $has_new_version ? 'Found newer plugin version ' . $latest_tag->version : 'No new version' ); $is_latest_version_beta = ( 'beta' === $latest_tag->release_mode ); $this->_storage->beta_data = array( 'is_beta' => $is_latest_version_beta, 'version' => $latest_tag->version ); return $has_new_version ? $latest_tag : false; } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * * @param bool|number $plugin_id * @param bool $flush Since 1.1.7.3 * @param int $expiration Since 1.2.2.7 * @param bool|string $newer_than Since 2.2.1 * * @return bool|FS_Plugin_Tag */ function get_update( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) { $this->_logger->entrance(); if ( ! is_numeric( $plugin_id ) ) { $plugin_id = $this->_plugin->id; } $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than ); $updates = $this->get_all_updates(); return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false; } /** * Check if site assigned with active license. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @deprecated Please use has_active_valid_license() instead because license can be cancelled. */ function has_active_license() { return ( is_object( $this->_license ) && is_numeric( $this->_license->id ) && ! $this->_license->is_expired() ); } /** * Check if site assigned with active & valid (not expired) license. * * @author Vova Feldman (@svovaf) * @since 1.2.1 * * @param bool $check_expiration */ function has_active_valid_license( $check_expiration = true ) { return self::is_active_valid_license( $this->_license, $check_expiration ); } /** * @author Leo Fajardo (@leorw) * @since 2.3.1 */ function is_data_debug_mode() { if ( is_null( $this->is_whitelabeled ) || ! $this->is_whitelabeled ) { return false; } $fs = $this->is_addon() ? $this->get_parent_instance() : $this; if ( $fs->is_network_active() && fs_is_network_admin() ) { $is_developer_license_debug_mode = get_site_transient( "fs_{$this->get_id()}_data_debug_mode" ); } else { $is_developer_license_debug_mode = get_transient( "fs_{$this->get_id()}_data_debug_mode" ); } return ( 'true' === $is_developer_license_debug_mode ); } /** * @author Leo Fajardo (@leorw) * @since 2.3.1 */ function _set_data_debug_mode() { if ( ! $this->is_whitelabeled( true ) ) { return; } $license_or_user_key = fs_request_get_raw( 'license_or_user_key' ); $transient_value = ( ! empty( $license_or_user_key ) ) ? 'true' : 'false'; if ( 'true' === $transient_value ) { $stored_key = $this->_storage->get( ! FS_User::is_valid_id( $this->_storage->last_license_user_id ) ? 'last_license_key' : 'last_license_user_key' ); if ( md5( $license_or_user_key ) !== $stored_key ) { $this->shoot_ajax_failure( sprintf( '%s... %s', $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ), $this->get_text_inline( 'seems like the key you entered doesn\'t match our records.', 'developer-or-license-not-found' ) ) ); } } if ( $this->is_network_active() && fs_is_network_admin() ) { set_site_transient( "fs_{$this->get_id()}_data_debug_mode", $transient_value, WP_FS__TIME_24_HOURS_IN_SEC / 24 ); } else { set_transient( "fs_{$this->get_id()}_data_debug_mode", $transient_value, WP_FS__TIME_24_HOURS_IN_SEC / 24 ); } if ( 'true' === $transient_value ) { $this->_admin_notices->add_sticky( $this->get_text_inline( 'Debug mode was successfully enabled and will be automatically disabled in 60 min. You can also disable it earlier by clicking the "Stop Debug" link.', 'data_debug_mode_enabled' ), 'data_debug_mode_enabled' ); } $this->shoot_ajax_success(); } /** * Check if a given license is active & valid (not expired). * * @author Vova Feldman (@svovaf) * @since 2.1.3 * * @param FS_Plugin_License $license * @param bool $check_expiration * * @return bool */ private static function is_active_valid_license( $license, $check_expiration = true ) { return ( is_object( $license ) && FS_Plugin_License::is_valid_id( $license->id ) && $license->is_active() && ( ! $check_expiration || $license->is_valid() ) ); } /** * Checks if there's any site that is associated with an active & valid license. * This logic is used to determine if the admin can download the premium code base from a network level admin. * * @author Vova Feldman (@svovaf) * @since 2.1.3 * * @return bool */ function has_any_active_valid_license() { if ( ! fs_is_network_admin() ) { return $this->has_active_valid_license(); } $installs = $this->get_blog_install_map(); $all_plugin_licenses = self::get_all_licenses( $this->_module_id ); foreach ( $installs as $blog_id => $install ) { if ( ! FS_Plugin_License::is_valid_id( $install->license_id ) ) { continue; } foreach ( $all_plugin_licenses as $license ) { if ( $license->id == $install->license_id ) { if ( self::is_active_valid_license( $license ) ) { return true; } } } } return false; } /** * Check if site assigned with license with enabled features. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @return bool */ function has_features_enabled_license() { return ( is_object( $this->_license ) && is_numeric( $this->_license->id ) && $this->_license->is_features_enabled() ); } /** * Checks if the product is activated with a bundle license. * * @author Leo Fajardo (@leorw) * @since 2.4.0 * * @return bool */ function is_activated_with_bundle_license() { if ( ! $this->has_features_enabled_license() ) { return false; } return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id ); } /** * Check if user is a trial or have feature enabled license. * * @author Vova Feldman (@svovaf) * @since 1.1.7 * * @return bool */ function can_use_premium_code() { return $this->is_trial() || $this->has_features_enabled_license(); } /** * Checks if the current user can activate plugins or switch themes. Note that this method should only be used * after the `init` action is triggered because it is using `current_user_can()` which is only functional after * the context user is authenticated. * * @author Leo Fajardo (@leorw) * @since 1.2.2 * * @return bool */ function is_user_admin() { /** * Require a super-admin when network activated, running from the network level OR if * running from the site level but not delegated the opt-in. * * @author Vova Feldman (@svovaf) * @since 2.0.0 */ if ( $this->_is_network_active && ( fs_is_network_admin() || ! $this->is_delegated_connection() ) ) { return is_super_admin(); } return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) ) || ( $this->is_theme() && current_user_can( 'switch_themes' ) ); } /** * Sync site's plan. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * * @uses FS_Api * * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by * the admin. * @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the * network-level "Account" page. * @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the * `_sync_plugin_license` method in order to switch to the previous blog when sending * updates for a single site in case `execute_cron` has switched to a different blog. */ private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) { $this->_logger->entrance(); $plugin_id = fs_request_get( 'plugin_id', $this->get_id() ); $is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() ); if ( $is_addon_sync ) { $this->_sync_addon_license( $plugin_id, $background ); } else { $this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id ); } $this->do_action( 'after_account_plan_sync', $this->get_plan_name() ); } /** * Sync plugin's add-on license. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * @uses FS_Api * * @param number $addon_id * @param bool $background */ private function _sync_addon_license( $addon_id, $background ) { $this->_logger->entrance(); if ( $this->is_addon_activated( $addon_id ) ) { // If already installed, use add-on sync. $fs_addon = self::get_instance_by_id( $addon_id ); if ( // Add-on is network activated and network integrated. $fs_addon->is_network_active() || // Background sync cron. self::is_cron() || // Add-on is not network activated or not network integrated. ! fs_is_network_admin() ) { $fs_addon->_sync_license( $background ); return; } } // Validate add-on exists. $addon = $this->get_addon( $addon_id ); if ( ! is_object( $addon ) ) { return; } // Add add-on into account add-ons. $account_addons = $this->get_account_addons(); if ( ! is_array( $account_addons ) ) { $account_addons = array(); } $account_addons[] = $addon->id; $account_addons = array_unique( $account_addons ); $this->_store_account_addons( $account_addons ); // Load add-on licenses. $licenses = $this->_fetch_licenses( $addon->id ); // Sync add-on licenses. if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) { $this->_update_licenses( $licenses, $addon->id ); if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) { $plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) ); if ( ! isset( $plans_result->error ) ) { $plans = array(); foreach ( $plans_result->plans as $plan ) { $plans[] = new FS_Plugin_Plan( $plan ); } $this->_admin_notices->add_sticky( sprintf( ( FS_Plan_Manager::instance()->has_free_plan( $plans ) ? $this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) : /* translators: %s:product name, e.g. Facebook add-on was successfully... */ $this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ), $addon->title ) . ' ' . $this->get_latest_download_link( $this->get_text_inline( 'Download the latest version', 'download-latest-version' ), $addon_id ), 'addon_plan_upgraded_' . $addon->slug, $this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!' ); } } } } /** * Sync site's plugin plan. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * @uses FS_Api * * @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin. * @param bool $send_installs_update Since 2.0.0 * @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and * syncing its license from the network-level "Account" page (e.g.: after * activating a license only for the single install). * @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it * can be used here to switch to the previous blog in case `execute_cron` * has switched to a different blog. */ private function _sync_plugin_license( $background = false, $send_installs_update = true, $is_context_single_site = false, $current_blog_id = null ) { $this->_logger->entrance(); $plan_change = 'none'; $is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active ); if ( ! $send_installs_update ) { $site = $this->_site; } else { /** * Sync site info. * * @todo This line will execute install sync on a daily basis, even if running the free version (for opted-in users). The reason we want to keep it that way is for cases when the user was a paying customer, then there was a failure in subscription payment, and then after some time the payment was successful. This could be heavily optimized. For example, we can skip the $flush if the current install was never associated with a paid version. */ if ( $is_site_level_sync ) { /** * Switch to the previous blog since `execute_cron` may have switched to a different blog. * * @author Leo Fajardo (@leorw) * @since 2.2.3 */ if ( is_numeric( $current_blog_id ) ) { $this->switch_to_blog( $current_blog_id ); } $result = $this->send_install_update( array(), true, true ); $is_valid = $this->is_api_result_entity( $result ); } else { $result = $this->send_installs_update( array(), true, true ); $is_valid = $this->is_api_result_object( $result, 'installs' ); } if ( ! $is_valid ) { if ( $is_context_single_site ) { // Switch back to the main blog so that the following logic will have the right entities. $this->switch_to_blog( $this->_storage->network_install_blog_id ); } // 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::is_blocked( $result ) ) { /** * @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. */ if ( ! self::$_global_admin_notices->has_sticky( '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 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' ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ), '', 'error' ); } } // No reason to continue with license sync while there are API issues. 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 { // Map site addresses to their blog IDs. $address_to_blog_map = $this->get_address_to_blog_map(); // Find the current context install. $site = null; foreach ( $result->installs as $install ) { if ( $install->id == $this->_site->id ) { $site = new FS_Site( $install ); } else { $address = trailingslashit( fs_strip_url_protocol( $install->url ) ); $blog_id = $address_to_blog_map[ $address ]; $this->_store_site( true, $blog_id, new FS_Site( $install ) ); } } } // Sync plans. $this->_sync_plans(); } // Remove sticky API connectivity message. self::$_global_admin_notices->remove_sticky( 'api_blocked' ); if ( ! $this->has_paid_plan() ) { $this->_site = $site; $this->_store_site( true, $is_site_level_sync ? null : $this->get_network_install_blog_id() ); } else { $context_blog_id = 0; if ( $is_context_single_site ) { $context_blog_id = get_current_blog_id(); // Switch back to the main blog in order to properly sync the license. $this->switch_to_blog( $this->_storage->network_install_blog_id ); } /** * Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license * associated with that ID is not included in the user's licenses collection. */ $this->_sync_licenses( $site->license_id, ( $is_context_single_site ? $context_blog_id : null ) ); if ( $is_context_single_site ) { $this->switch_to_blog( $context_blog_id ); } // Check if plan / license changed. if ( $site->plan_id != $this->_site->plan_id || // Check if trial started. $site->trial_plan_id != $this->_site->trial_plan_id || $site->trial_ends != $this->_site->trial_ends || // Check if license changed. $site->license_id != $this->_site->license_id ) { if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) { // New trial started. $this->_site = $site; $plan_change = 'trial_started'; // For trial with subscription use-case. $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id ); if ( is_object( $new_license ) && $new_license->is_valid() ) { $this->_site = $site; $this->_update_site_license( $new_license ); $this->_store_licenses(); $this->_sync_site_subscription( $this->_license ); } } else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) { // Was in trial, but now trial expired and no license ID. // New trial started. $this->_site = $site; $plan_change = 'trial_expired'; } else { $is_free = $this->is_free_plan(); // Make sure license exist and not expired. $new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id ); if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) { // License cancelled. $this->_site = $site; $this->_update_site_license( $new_license ); $this->_store_licenses(); $plan_change = 'cancelled'; } else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) { // The license is expired, so ignore upgrade method. $this->_site = $site; } else { // License changed. $this->_site = $site; /** * IMPORTANT: * The line below should be executed before trying to activate the license on the rest of the network, otherwise, the license' activation counters may be out of sync + there's no need to activate the license on the context site since it's already activated on it. * * @author Vova Feldman (@svovaf) * @since 2.0.0 */ $this->_update_site_license( $new_license ); if ( ! $is_context_single_site && fs_is_network_admin() && $this->_is_network_active && $new_license->quota > 1 && get_blog_count() > 1 ) { // See if license can activated on all sites. if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) { if ( ! fs_request_get_bool( 'auto_install' ) ) { // Open the license activation dialog box on the account page. add_action( 'admin_footer', array( &$this, '_open_license_activation_dialog_box' ) ); } } } $this->_store_licenses(); $plan_change = $is_free ? ( $this->is_only_premium() ? 'activated' : 'upgraded' ) : ( is_object( $new_license ) ? 'changed' : 'downgraded' ); } } // Store updated site info. $this->_store_site( true, $is_site_level_sync ? null : $this->get_network_install_blog_id() ); } else { if ( ! is_object( $this->_license ) ) { $this->maybe_update_whitelabel_flag( FS_Plugin_License::is_valid_id( $site->license_id ) ? $this->get_license_by_id( $site->license_id ) : null ); } else { $this->maybe_update_whitelabel_flag( $this->_license ); if ( $this->_license->is_expired() ) { if ( ! $this->has_features_enabled_license() ) { $this->_deactivate_license(); $plan_change = 'downgraded'; } else { $last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 ); if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) { /** * Show the expired license notice every 14 days. * * @author Leo Fajardo (@leorw) * @since 2.3.1 */ $plan_change = 'expired'; } } } } if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) { $this->_sync_site_subscription( $this->_license ); } } if ( ! $this->is_addon() && $this->_site->is_beta() !== $site->is_beta() ) { // Beta flag updated. $this->_site = $site; $this->_store_site( true, $is_site_level_sync ? null : $this->get_network_install_blog_id() ); } if ( $this->is_addon() || $this->has_addons() ) { /** * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded, * an updated valid user licenses collection will be fetched from the server which is used to also * update the account add-ons (add-ons the user has licenses for). * * @author Leo Fajardo (@leorw) * @since 2.2.4 */ $this->purge_valid_user_licenses_cache(); } } $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'; if ( $this->apply_filters( 'has_paid_plan_account', $this->has_paid_plan() ) ) { switch ( $plan_change ) { case 'none': if ( ! $background && is_admin() ) { $plan = $this->is_trial() ? $this->get_trial_plan() : $this->get_plan(); if ( $plan->is_free() ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( 'It looks like you are still on the %s plan. If you did upgrade or change your plan, it\'s probably an issue on our side - sorry.', 'plan-did-not-change-message' ), '<i><b>' . $plan->title . ( $this->is_trial() ? ' ' . $this->get_text_x_inline( 'Trial', 'trial period', 'trial' ) : '' ) . '</b></i>' ) . ' ' . sprintf( '<a href="%s">%s</a>', $this->contact_url( 'bug', sprintf( $this->get_text_inline( 'I have upgraded my account but when I try to Sync the License, the plan remains %s.', 'plan-did-not-change-email-message' ), strtoupper( $plan->name ) ) ), $this->get_text_inline( 'Please contact us here', 'contact-us-here' ) ), $hmm_text ); } } break; case 'upgraded': case 'activated': $this->add_after_plan_activation_or_upgrade_instructions_notice( 'upgraded' === $plan_change ); $this->_admin_notices->remove_sticky( array( 'trial_started', 'trial_promotion', 'trial_expired', 'activation_complete', 'license_expired', ) ); break; case 'changed': $this->_admin_notices->add_sticky( sprintf( $this->get_text_inline( 'Your plan was successfully changed to %s.', 'plan-changed-to-x-message' ), $this->get_plan_title() ), 'plan_changed' ); $this->_admin_notices->remove_sticky( array( 'trial_started', 'trial_promotion', 'trial_expired', 'activation_complete', ) ); break; case 'downgraded': $this->_admin_notices->add_sticky( ($this->has_free_plan() ? sprintf( $this->get_text_inline( 'Your license has expired. You can still continue using the free %s forever.', 'license-expired-blocking-message' ), $this->_module_type ) : /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */ sprintf( $this->get_text_inline( 'Your license has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions.', 'license-expired-blocking-message_premium-only' ), sprintf('<a href="%s">', $this->pricing_url()), '</a>', $this->get_module_label(true) ) ), 'license_expired', $hmm_text ); $this->_admin_notices->remove_sticky( 'plan_upgraded' ); break; case 'cancelled': $this->_admin_notices->add( $this->get_text_inline( 'Your license has been cancelled. If you think it\'s a mistake, please contact support.', 'license-cancelled' ) . ' ' . sprintf( '<a href="%s">%s</a>', $this->contact_url( 'bug' ), $this->get_text_inline( 'Please contact us here', 'contact-us-here' ) ), $hmm_text, 'error' ); $this->_admin_notices->remove_sticky( 'plan_upgraded' ); break; case 'expired': $this->_admin_notices->add_sticky( sprintf( $this->get_text_inline( 'Your license has expired. You can still continue using all the %s features, but you\'ll need to renew your license to continue getting updates and support.', 'license-expired-non-blocking-message' ), $this->get_plan()->title ), 'license_expired', $hmm_text ); $this->_storage->expired_license_notice_shown = WP_FS__SCRIPT_START_TIME; $this->_admin_notices->remove_sticky( 'plan_upgraded' ); break; case 'trial_started': $this->add_complete_upgrade_instructions_notice( sprintf( $this->get_text_inline( 'Your trial has been successfully started.', 'trial-started-message' ), '<i>' . $this->get_plugin_name() . '</i>' ), 'trial_started', $this->get_trial_plan()->title ); $this->_admin_notices->remove_sticky( array( 'trial_promotion', ) ); break; case 'trial_expired': $this->_admin_notices->add_sticky( ($this->has_free_plan() ? $this->get_text_inline( 'Your free trial has expired. You can still continue using all our free features.', 'trial-expired-message' ) : /* translators: %1$s: product title; %2$s, %3$s: wrapping HTML anchor element; %4$s: 'plugin', 'theme', or 'add-on'. */ sprintf( $this->get_text_inline( 'Your free trial has expired. %1$sUpgrade now%2$s to continue using the %3$s without interruptions.', 'trial-expired-message_premium-only' ), sprintf('<a href="%s">', $this->pricing_url()), '</a>', $this->get_module_label(true))), 'trial_expired', $hmm_text ); $this->_admin_notices->remove_sticky( array( 'trial_started', 'trial_promotion', 'plan_upgraded', ) ); break; } } if ( 'none' !== $plan_change ) { if ( ! is_object( $this->_license ) || ! $this->_license->is_whitelabeled ) { $this->_admin_notices->remove_sticky( 'license_whitelabeled' ); } $this->do_action( 'after_license_change', $plan_change, $this->get_plan() ); } } /** * @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 .= "<li>{$api_domain}</li>"; } $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(), "<ol>{$api_domains_list_items}</ol><a href='#' class='fs-api-request-error-show-details-link'>" . $this->get_text_inline( 'Show error details', 'show-error-details' ) . " <span class='dashicons dashicons-arrow-down-alt2'></span></a>" ); $error_message = "<div>{$error_message}</div>" . '<div class="fs-api-request-error-details" style="display: none">' . '<strong>' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . '</strong><br>' . $result->error->message . '</div>'; return $error_message; } /** * Include the required JS at the footer of the admin to trigger the license activation dialog box. * * @author Vova Feldman (@svovaf) * @since 2.0.0 */ public function _open_license_activation_dialog_box() { $vars = array( 'license_id' => $this->_site->license_id ); fs_require_once_template( 'js/open-license-activation.php', $vars ); } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * * @param bool $background * @param FS_Plugin_License|null $premium_license */ protected function _activate_license( $background = false, $premium_license = null ) { $this->_logger->entrance(); if ( is_null( $premium_license ) ) { $license_id = fs_request_get( 'license_id' ); if ( is_object( $this->_site ) && FS_Plugin_License::is_valid_id( $license_id ) && $license_id == $this->_site->license_id ) { // License is already activated. return; } $premium_license = FS_Plugin_License::is_valid_id( $license_id ) ? $this->_get_license_by_id( $license_id ) : $this->_get_available_premium_license(); } if ( ! is_object( $premium_license ) ) { return; } if ( ! is_object( $this->_site ) ) { // Not yet opted-in. $user = $this->get_current_or_network_user(); if ( ! is_object( $user ) ) { $user = self::_get_user_by_id( $premium_license->user_id ); } if ( is_object( $user ) ) { $this->install_with_user( $user, $premium_license->secret_key, false, false, false ); } else { $this->opt_in( false, false, false, $premium_license->secret_key ); return; } } /** * If the premium license is already associated with the install, just * update the license reference (activation is not required). * * @since 1.1.9 */ if ( $premium_license->id == $this->_site->license_id ) { // License is already activated. $this->_update_site_license( $premium_license ); $this->_store_account(); return; } if ( $this->_site->user_id != $premium_license->user_id ) { $api_request_params = array( 'license_key' => $premium_license->secret_key ); } else { $api_request_params = array(); } $api = $this->get_api_site_scope(); $license = $api->call( "/licenses/{$premium_license->id}.json?is_enriched=true", 'put', $api_request_params ); if ( ! $this->is_api_result_entity( $license ) ) { if ( ! $background ) { $this->_admin_notices->add( sprintf( '%s %s', $this->get_text_inline( 'It looks like the license could not be activated.', 'license-activation-failed-message' ), ( is_object( $license ) && isset( $license->error ) ? $license->error->message : sprintf( '%s<br><code>%s</code>', $this->get_text_inline( 'Error received from the server:', 'server-error-message' ), var_export( $license, true ) ) ) ), $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...', 'error' ); } return; } $premium_license = new FS_Plugin_License( $license ); // Updated site plan. $site = $this->get_api_site_scope()->get( '/', true ); if ( $this->is_api_result_entity( $site ) ) { $this->_site = new FS_Site( $site ); } $this->_update_site_license( $premium_license ); $this->_store_account(); if ( $this->is_addon() || $this->has_addons() ) { /** * Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded, * an updated valid user licenses collection will be fetched from the server which is used to also * update the account add-ons (add-ons the user has licenses for). * * @author Leo Fajardo (@leorw) * @since 2.2.4 */ $this->purge_valid_user_licenses_cache(); } if ( ! $background ) { $this->add_complete_upgrade_instructions_notice( $this->get_text_inline( 'Your license was successfully activated.', 'license-activated-message' ), 'license_activated' ); } $this->_admin_notices->remove_sticky( array( 'trial_promotion', 'license_expired', ) ); } /** * @author Vova Feldman (@svovaf) * @since 1.0.5 * * @param bool $show_notice */ protected function _deactivate_license( $show_notice = true ) { $this->_logger->entrance(); $hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...'; if ( ! FS_Plugin_License::is_valid_id( $this->_site->license_id ) ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( 'It looks like your site currently doesn\'t have an active license.', 'no-active-license-message' ), $this->get_plan_title() ), $hmm_text ); return; } $api = $this->get_api_site_scope(); $license = $api->call( "/licenses/{$this->_site->license_id}.json", 'delete' ); $this->handle_license_deactivation_result( $license, $hmm_text, $show_notice ); } /** * @author Leo Fajardo (@leorw) * @since 2.2.1 * * @param FS_Plugin_License $license * @param bool|string $hmm_text * @param bool $show_notice */ private function handle_license_deactivation_result( $license, $hmm_text = false, $show_notice = true ) { if ( isset( $license->error ) ) { $this->_admin_notices->add( $this->get_text_inline( 'It looks like the license deactivation failed.', 'license-deactivation-failed-message' ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . ' ' . var_export( $license->error, true ), $hmm_text, 'error' ); return; } // Update license cache. if ( is_array( $this->_licenses ) ) { for ( $i = 0, $len = count( $this->_licenses ); $i < $len; $i ++ ) { if ( $license->id == $this->_licenses[ $i ]->id ) { $this->_licenses[ $i ] = new FS_Plugin_License( $license ); } } } // Update site plan to default. $this->_sync_plans(); $this->_site->plan_id = $this->_plans[0]->id; // Unlink license from site. $this->_update_site_license( null ); $this->_store_account(); if ( $show_notice ) { $this->_admin_notices->add( sprintf( $this->is_only_premium() ? $this->get_text_inline( 'Your %s license was successfully deactivated.', 'license-deactivation-message_premium-only' ) : $this->get_text_inline( 'Your license was successfully deactivated, you are back to the %s plan.', 'license-deactivation-message' ), $this->get_plan_title() ), $this->get_text_inline( 'O.K', 'ok' ) ); } $this->_admin_notices->remove_sticky( array( 'plan_upgraded', 'license_activated', ) ); } /** * Site plan downgrade. * * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @return object * * @uses FS_Api */ private function _downgrade_site() { $this->_logger->entrance(); $deactivate_license = fs_request_get_bool( 'deactivate_license' ); $api = $this->get_api_site_scope(); $site = $api->call( 'downgrade.json', 'put', array( 'deactivate_license' => $deactivate_license ) ); $plan_downgraded = false; $plan = false; if ( $this->is_api_result_entity( $site ) ) { $prev_plan_id = $this->_site->plan_id; // Update new site plan id. $this->_site->plan_id = $site->plan_id; $plan = $this->get_plan(); $subscription = $this->_sync_site_subscription( $this->_license ); // Plan downgraded if plan was changed or subscription was cancelled. $plan_downgraded = ( $plan instanceof FS_Plugin_Plan && $prev_plan_id != $plan->id ) || ( is_object( $subscription ) && ! isset( $subscription->error ) && ! $subscription->is_active() ); } else { // handle different error cases. $this->handle_license_deactivation_result( $site, $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...' ); } if ( ! $plan_downgraded ) { return (object) array( 'error' => (object) array( 'message' => $this->get_text_inline( 'Seems like we are having some temporary issue with your subscription cancellation. Please try again in few minutes.', 'subscription-cancellation-failure-message' ) ) ); } // Remove previous sticky message about upgrade (if exist). $this->_admin_notices->remove_sticky( 'plan_upgraded' ); $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Your subscription was successfully cancelled. Your %s plan license will expire in %s.', 'plan-x-downgraded-message' ), $plan->title, human_time_diff( time(), strtotime( $this->_license->expiration ) ) ) ); // Store site updates. $this->_store_site(); if ( $deactivate_license && ! FS_Plugin_License::is_valid_id( $site->license_id ) ) { if ( $this->_site->is_localhost() ) { $this->_license->activated_local = max( 0, $this->_license->activated_local - 1 ); } else { $this->_license->activated = max( 0, $this->_license->activated - 1 ); } // Handle successful license deactivation result. $this->handle_license_deactivation_result( $this->_license ); } return $site; } /** * @author Vova Feldman (@svovaf) * @since 1.1.8.1 * * @param bool|string $plan_name * * @return bool If trial was successfully started. */ function start_trial( $plan_name = false ) { $this->_logger->entrance(); // Alias. $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...'; if ( $this->is_trial() ) { // Already in trial mode. $this->_admin_notices->add( sprintf( $this->get_text_inline( 'You are already running the %s in a trial mode.', 'in-trial-mode' ), $this->_module_type ), $oops_text, 'error' ); return false; } if ( $this->_site->is_trial_utilized() ) { // Trial was already utilized. $this->_admin_notices->add( $this->get_text_inline( 'You already utilized a trial before.', 'trial-utilized' ), $oops_text, 'error' ); return false; } if ( false !== $plan_name ) { $plan = $this->get_plan_by_name( $plan_name ); if ( false === $plan ) { // Plan doesn't exist. $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Plan %s do not exist, therefore, can\'t start a trial.', 'trial-plan-x-not-exist' ), $plan_name ), $oops_text, 'error' ); return false; } if ( ! $plan->has_trial() ) { // Plan doesn't exist. $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Plan %s does not support a trial period.', 'plan-x-no-trial' ), $plan_name ), $oops_text, 'error' ); return false; } } else { if ( ! $this->has_trial_plan() ) { // None of the plans have a trial. $this->_admin_notices->add( sprintf( $this->get_text_inline( 'None of the %s\'s plans supports a trial period.', 'no-trials' ), $this->_module_type ), $oops_text, 'error' ); return false; } $plans_with_trial = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans ); $plan = $plans_with_trial[0]; } $api = $this->get_api_site_scope(); $plan = $api->call( "plans/{$plan->id}/trials.json", 'post' ); if ( ! $this->is_api_result_entity( $plan ) ) { // Some API error while trying to start the trial. $this->_admin_notices->add( $this->get_api_error_message( $plan ), $oops_text, 'error' ); return false; } // Sync license. $this->_sync_license(); return $this->is_trial(); } /** * Cancel site trial. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return object * * @uses FS_Api */ private function _cancel_trial() { $this->_logger->entrance(); if ( ! $this->is_trial() ) { return (object) array( 'error' => (object) array( 'message' => $this->get_text_inline( 'It looks like you are not in trial mode anymore so there\'s nothing to cancel :)', 'trial-cancel-no-trial-message' ) ) ); } $trial_plan = $this->get_trial_plan(); $api = $this->get_api_site_scope(); $site = $api->call( 'trials.json', 'delete' ); $trial_cancelled = false; if ( $this->is_api_result_entity( $site ) ) { $prev_trial_ends = $this->_site->trial_ends; if ( $this->is_paid_trial() ) { $this->_license->expiration = $site->trial_ends; $this->_license->is_cancelled = true; $this->_update_site_license( $this->_license ); $this->_store_licenses(); // Clear subscription reference. $this->_sync_site_subscription( null ); } // Update site info. $this->_site = new FS_Site( $site ); $trial_cancelled = ( $prev_trial_ends != $site->trial_ends ); } else { // @todo handle different error cases. } if ( ! $trial_cancelled ) { return (object) array( 'error' => (object) array( 'message' => $this->get_text_inline( 'Seems like we are having some temporary issue with your trial cancellation. Please try again in few minutes.', 'trial-cancel-failure-message' ) ) ); } // Remove previous sticky messages about upgrade or trial (if exist). $this->_admin_notices->remove_sticky( array( 'trial_started', 'trial_promotion', 'plan_upgraded', ) ); // Store site updates. $this->_store_site(); if ( ! $this->is_addon() || ! $this->deactivate_premium_only_addon_without_license( true ) ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Your %s free trial was successfully cancelled.', 'trial-cancel-message' ), $trial_plan->title ) ); } return $site; } /** * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @param bool|number $plugin_id * * @return bool */ private function _is_addon_id( $plugin_id ) { return is_numeric( $plugin_id ) && ( $this->get_id() != $plugin_id ); } /** * Check if user eligible to download premium version updates. * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @return bool */ private function _can_download_premium() { return $this->has_any_active_valid_license() || ( $this->is_trial() && ! $this->get_trial_plan()->is_free() ); } /** * * @author Vova Feldman (@svovaf) * @since 1.0.6 * * @param bool|number $addon_id * @param string $type "json" or "zip" * * @return string */ private function _get_latest_version_endpoint( $addon_id = false, $type = 'json' ) { $is_addon = $this->_is_addon_id( $addon_id ); $is_premium = null; if ( ! $is_addon ) { $is_premium = ( $this->is_premium() || $this->_can_download_premium() ); } else if ( $this->is_addon_activated( $addon_id ) ) { $fs_addon = self::get_instance_by_id( $addon_id ); $is_premium = ( $fs_addon->is_premium() || $fs_addon->_can_download_premium() ); } // If add-on, then append add-on ID. $endpoint = ( $is_addon ? "/addons/$addon_id" : '' ) . '/updates/latest.' . $type; // If add-on and not yet activated, try to fetch based on server licensing. if ( is_bool( $is_premium ) ) { $endpoint = add_query_arg( 'is_premium', json_encode( $is_premium ), $endpoint ); } if ( $this->has_secret_key() ) { $endpoint = add_query_arg( 'type', 'all', $endpoint ); } else if ( is_object( $this->_site ) && $this->_site->is_beta() ) { $endpoint = add_query_arg( 'type', 'beta', $endpoint ); } return $endpoint; } /** * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @param bool|number $addon_id * @param bool $flush Since 1.1.7.3 * @param int $expiration Since 1.2.2.7 * @param bool|string $newer_than Since 2.2.1 * @param bool|string $fetch_readme Since 2.2.1 * * @return object|false Plugin latest tag info. */ function _fetch_latest_version( $addon_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false, $fetch_readme = true ) { $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() || ! FS_Permission_Manager::instance( $this )->is_essentials_tracking_allowed() ) && ! $this->_is_addon_id( $addon_id ) ) { if ( ! is_multisite() ) { return false; } $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 */ if ( $install->is_trial() ) { $switch_to_blog_id = $blog_id; break; } if ( FS_Plugin_License::is_valid_id( $install->license_id ) ) { $license = $this->get_license_by_id( $install->license_id ); if ( is_object( $license ) && $license->is_features_enabled() ) { $switch_to_blog_id = $blog_id; break; } } } if ( is_null( $switch_to_blog_id ) ) { return false; } } $current_blog_id = is_numeric( $switch_to_blog_id ) ? get_current_blog_id() : 0; if ( is_numeric( $switch_to_blog_id ) ) { $this->switch_to_blog( $switch_to_blog_id ); } $latest_version_endpoint = $this->_get_latest_version_endpoint( $addon_id, 'json' ); if ( ! empty( $newer_than ) ) { $latest_version_endpoint = add_query_arg( 'newer_than', $newer_than, $latest_version_endpoint ); } if ( true === $fetch_readme ) { $latest_version_endpoint = add_query_arg( 'readme', 'true', $latest_version_endpoint ); } $tag = $this->get_api_site_or_plugin_scope()->get( $latest_version_endpoint, $flush, $expiration ); if ( is_numeric( $switch_to_blog_id ) ) { $this->switch_to_blog( $current_blog_id ); } $latest_version = ( is_object( $tag ) && isset( $tag->version ) ) ? $tag->version : 'couldn\'t get'; $this->_logger->departure( 'Latest version ' . $latest_version ); return ( is_object( $tag ) && isset( $tag->version ) ) ? $tag : false; } #---------------------------------------------------------------------------------- #region Download Plugin #---------------------------------------------------------------------------------- /** * Download latest plugin version, based on plan. * * Not like _download_latest(), this will redirect the page * to secure download url to prevent dual download (from FS to WP server, * and then from WP server to the client / browser). * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @param bool|number $plugin_id * * @uses FS_Api * @uses wp_redirect() */ private function download_latest_directly( $plugin_id = false ) { $this->_logger->entrance(); wp_redirect( $this->get_latest_download_api_url( $plugin_id ) ); } /** * Get latest plugin FS API download URL. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @param bool|number $plugin_id * * @return string */ private function get_latest_download_api_url( $plugin_id = false ) { $this->_logger->entrance(); $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 ); } /** * Get payment invoice URL. * * @author Vova Feldman (@svovaf) * @since 1.2.0 * * @param bool|number $payment_id * * @return string */ function _get_invoice_api_url( $payment_id = false ) { $this->_logger->entrance(); $url = $this->get_api_user_scope()->get_signed_url( "/payments/{$payment_id}/invoice.pdf" ); if ( ! fs_starts_with( $url, 'https://' ) ) { // Always use HTTPS for invoices. $url = 'https' . substr( $url, 4 ); } return $url; } /** * Get latest plugin download link. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @param string $label * @param bool|number $plugin_id * * @return string */ private function get_latest_download_link( $label, $plugin_id = false ) { return sprintf( '<a target="_blank" rel="noopener" href="%s">%s</a>', $this->_get_latest_download_local_url( $plugin_id ), $label ); } /** * Get latest plugin download local URL. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @param bool|number $plugin_id * * @return string */ function _get_latest_download_local_url( $plugin_id = false ) { // Add timestamp to protect from caching. $params = array( 'ts' => WP_FS__SCRIPT_START_TIME ); if ( ! empty( $plugin_id ) ) { $params['plugin_id'] = $plugin_id; } else if ( $this->is_addon() ) { $params['plugin_id'] = $this->get_id(); } $fs = $this->is_addon() ? $this->get_parent_instance() : $this; return $this->apply_filters( 'download_latest_url', $fs->get_account_url( 'download_latest', $params ) ); } #endregion Download Plugin ------------------------------------------------------------------ /** * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @uses FS_Api * * @param bool $background Hints the method if it's a background updates check. If false, it means that * was initiated by the admin. * @param bool|number $plugin_id * @param bool $flush Since 1.1.7.3 * @param int $expiration Since 1.2.2.7 * @param bool|string $newer_than Since 2.2.1 */ private function check_updates( $background = false, $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) { $this->_logger->entrance(); // Check if there's a newer version for download. $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than ); $update = null; if ( is_object( $new_version ) ) { $update = new FS_Plugin_Tag( $new_version ); if ( ! $background ) { $this->_admin_notices->add( sprintf( /* translators: %s: Numeric version number (e.g. '2.1.9' */ $this->get_text_inline( 'Version %s was released.', 'version-x-released' ) . ' ' . $this->get_text_inline( 'Please download %s.', 'please-download-x' ), $update->version, sprintf( '<a href="%s" target="_blank" rel="noopener">%s</a>', $this->get_account_url( 'download_latest' ), sprintf( /* translators: %s: plan name (e.g. latest "Professional" version) */ $this->get_text_inline( 'the latest %s version here', 'latest-x-version' ), $this->get_plan_title() ) ) ), $this->get_text_inline( 'New', 'new' ) . '!' ); } } else if ( false === $new_version && ! $background ) { $this->_admin_notices->add( $this->get_text_inline( 'Seems like you got the latest release.', 'you-have-latest' ), $this->get_text_inline( 'You are all good!', 'you-are-good' ) ); } $this->_store_update( $update, true, $plugin_id ); } /** * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @param bool $flush Since 1.1.7.3 add 24 hour cache by default. * * @return FS_Plugin[] * * @uses FS_Api */ private function sync_addons( $flush = false ) { $this->_logger->entrance(); $api = $this->get_api_site_or_plugin_scope(); $path = $this->add_show_pending( '/addons.json?enriched=true&count=50' ); /** * @since 1.2.1 * * If there's a cached version of the add-ons and not asking * for a flush, just use the currently stored add-ons. */ if ( ! $flush && $api->is_cached( $path ) ) { $addons = self::get_all_addons(); return isset( $addons[ $this->_plugin->id ] ) ? $addons[ $this->_plugin->id ] : array(); } $result = $api->get( $path, $flush ); $addons = array(); if ( $this->is_api_result_object( $result, 'plugins' ) && is_array( $result->plugins ) ) { for ( $i = 0, $len = count( $result->plugins ); $i < $len; $i ++ ) { $addons[ $i ] = new FS_Plugin( $result->plugins[ $i ] ); } $this->_store_addons( $addons, true ); } return $addons; } /** * Handle user email update. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * @uses FS_Api * * @param string $new_email * * @return object */ 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, 'after_email_confirm_url' => $this->_get_admin_page_url( 'account', array( 'fs_action' => 'sync_user' ) ), ) ); if ( ! isset( $user->error ) ) { $this->_user->email = $user->email; $this->_user->is_verified = $user->is_verified; $this->_store_user(); } else { // handle different error cases. } return $user; } #---------------------------------------------------------------------------------- #region API Error Handling #---------------------------------------------------------------------------------- /** * @author Vova Feldman (@svovaf) * @since 1.1.1 * * @param mixed $result * * @return bool Is API result contains an error. */ private function is_api_error( $result ) { return FS_Api::is_api_error( $result ); } /** * Checks if given API result is a non-empty and not an error object. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * * @param mixed $result * @param string|null $required_property Optional property we want to verify that is set. * * @return bool */ function is_api_result_object( $result, $required_property = null ) { return FS_Api::is_api_result_object( $result, $required_property ); } /** * Checks if given API result is a non-empty entity object with non-empty ID. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * * @param mixed $result * * @return bool */ private function is_api_result_entity( $result ) { return FS_Api::is_api_result_entity( $result ); } #endregion /** * Make sure a given argument is an array of a specific type. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * * @param mixed $array * @param string $class * * @return bool */ private function is_array_instanceof( $array, $class ) { return ( is_array( $array ) && ( empty( $array ) || $array[0] instanceof $class ) ); } /** * Start install ownership change. * * @author Vova Feldman (@svovaf) * @since 1.1.1 * @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, $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' ) ), ) ); return ! $this->is_api_error( $result ); } /** * Handle install ownership change. * * @author Vova Feldman (@svovaf) * @since 1.1.1 * @uses FS_Api * * @return bool Was ownership change successfully complete. */ private function complete_change_owner() { $this->_logger->entrance(); $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; } $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; } $this->set_account_option( 'sites', $current_blog_sites, true ); // Fetch new user information. $user_result = $this->get_api_user_scope( true )->get(); $user = new FS_User( $user_result ); $this->_user = $user; $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; } /** * Completes ownership change by license. * * @author Leo Fajardo (@leorw) * @since 2.3.2 * * @param number $user_id * @param array[string]number $install_ids_by_slug_map * */ private function complete_ownership_change_by_license( $user_id, $install_ids_by_slug_map ) { $this->_logger->entrance(); $this->sync_user_by_current_install( $user_id ); $result = $this->get_api_user_scope( true )->get( "/installs.json?install_ids=" . implode( ',', $install_ids_by_slug_map ) ); if ( $this->is_api_result_object( $result, 'installs' ) ) { $sites = self::get_all_sites( $this->get_module_type() ); $install_ids_by_slug_map = array_flip( $install_ids_by_slug_map ); foreach ( $result->installs as $install ) { $site = new FS_Site( $install ); $sites[ $install_ids_by_slug_map[ $site->id ] ] = clone $site; } $this->set_account_option( 'sites', $sites, true ); } } /** * Handle user name update. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * @uses FS_Api * * @return object */ private function update_user_name() { $this->_logger->entrance(); $name = fs_request_get( 'fs_user_name_' . $this->get_unique_affix(), '' ); $api = $this->get_api_user_scope(); $user = $api->call( "?plugin_id={$this->_plugin->id}&fields=id,first,last", 'put', array( 'name' => $name, ) ); if ( ! isset( $user->error ) ) { $this->_user->first = $user->first; $this->_user->last = $user->last; $this->_store_user(); } else { // handle different error cases. } return $user; } /** * Verify user email. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * @uses FS_Api */ private function verify_email() { $this->_handle_account_user_sync(); if ( $this->_user->is_verified() ) { return; } $api = $this->get_api_site_scope(); $result = $api->call( "/users/{$this->_user->id}/verify.json", 'put', array( 'after_email_confirm_url' => $this->_get_admin_page_url( 'account', array( 'fs_action' => 'sync_user' ) ) ) ); if ( ! isset( $result->error ) ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Verification mail was just sent to %s. If you can\'t find it after 5 min, please check your spam box.', 'verification-email-sent-message' ), sprintf( '<a href="mailto:%1$s">%2$s</a>', esc_url( $this->_user->email ), $this->_user->email ) ) ); } else { // handle different error cases. } } /** * @author Vova Feldman (@svovaf) * @since 1.1.2 * * @param array $params * @param bool|null $network * * @return string */ function get_activation_url( $params = array(), $network = null ) { if ( $this->is_addon() && $this->has_free_plan() ) { /** * @author Vova Feldman (@svovaf) * @since 1.2.1.7 Add-on's activation is the parent's module activation. */ return $this->get_parent_instance()->get_activation_url( $params ); } return $this->apply_filters( 'connect_url', $this->_get_admin_page_url( '', $params, $network ) ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.5 * * @param array $params * * @return string */ function get_reconnect_url( $params = array() ) { $params['fs_action'] = 'reset_anonymous_mode'; $params['fs_unique_affix'] = $this->get_unique_affix(); return $this->get_activation_url( $params ); } /** * Get the URL of the page that should be loaded after the user connect * or skip in the opt-in screen. * * @author Vova Feldman (@svovaf) * @since 1.1.3 * * @param string $filter Filter name. * @param array $params Since 1.2.2.7 * @param bool|null $network * * @return string */ function get_after_activation_url( $filter, $params = array(), $network = null ) { if ( $this->show_opt_in_on_themes_page() && ( fs_request_has( 'pending_activation' ) || // For cases when the first time path is set, even though it's a WP.org theme. fs_request_get_bool( $this->get_unique_affix() . '_show_optin' ) ) ) { $first_time_path = ''; } else { $first_time_path = $this->_menu->get_first_time_path( fs_is_network_admin() && $this->_is_network_active ); } if ( $this->_is_network_active && fs_is_network_admin() && ! $this->_menu->has_network_menu() && $this->is_network_registered() ) { $target_url = $this->get_account_url(); } else { // Default plugin's page. $target_url = $this->_get_admin_page_url( '', array(), $network ); } return add_query_arg( $params, $this->apply_filters( $filter, empty( $first_time_path ) ? $target_url : $first_time_path ) ); } /** * Handle account page updates / edits / actions. * * @author Vova Feldman (@svovaf) * @since 1.0.2 * */ private function _handle_account_edits() { if ( ! $this->is_user_admin() ) { return; } $action = fs_get_action(); if ( empty( $action ) ) { return; } $plugin_id = fs_request_get( 'plugin_id', $this->get_id() ); $install_id = fs_request_get( 'install_id', '' ); // Alias. $oops_text = $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...'; $is_network_action = $this->is_network_level_action(); $blog_id = $this->is_network_level_site_specific_action(); $is_parent_plugin_action = ( $plugin_id == $this->get_id() ); if ( is_numeric( $blog_id ) ) { $this->switch_to_blog( $blog_id ); } else { $blog_id = ''; } switch ( $action ) { case 'opt_in': check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); if ( $is_parent_plugin_action ) { if ( $is_network_action && ! empty( $blog_id ) ) { if ( ! $this->is_registered() ) { $this->install_with_user( $this->get_network_user(), false, false, false, false ); $this->_admin_notices->add( $this->get_text_inline( 'Site successfully opted in.', 'successful-opt-in' ), $this->get_text_inline( 'Awesome', 'awesome' ) ); } } } break; case 'toggle_tracking': check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); if ( $is_parent_plugin_action ) { if ( $is_network_action && ! empty( $blog_id ) ) { 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( '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' ), "<b>{$this->get_plugin_title()}</b>" ), $this->get_text_inline( 'Thank you!', 'thank-you' ) ); } } else { 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( 'Diagnostic data will no longer be sent from %s to %s.', 'opted-out-successfully' ), self::get_unfiltered_site_url( $blog_id, true ), "<b>{$this->get_plugin_title()}</b>" ) ); } } } } } break; case 'delete_account': check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); $is_network_deletion = $is_network_action && empty( $blog_id ); if ( $is_parent_plugin_action ) { // Delete add-on installs if have any. $installed_addons = $this->get_installed_addons(); foreach ( $installed_addons as $fs_addon ) { if ( $is_network_deletion ) { $fs_addon->delete_network_account_event(); } else { $fs_addon->delete_account_event(); } } if ( $is_network_deletion ) { $this->delete_network_account_event(); } else { $this->delete_account_event(); } // Clear user and site. $this->_site = null; $this->_user = null; $this->maybe_set_slug_and_network_menu_exists_flag(); fs_redirect( $this->get_activation_url() ); } else { if ( $this->is_addon_activated( $plugin_id ) ) { $fs_addon = self::get_instance_by_id( $plugin_id ); if ( $is_network_deletion ) { $fs_addon->delete_network_account_event(); } else { $fs_addon->delete_account_event(); } fs_redirect( $this->_get_admin_page_url( 'account' ) ); } } return; case 'downgrade_account': if ( is_numeric( $blog_id ) ) { check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); } else { check_admin_referer( $action ); } $switch_to_network_install_blog_after_cancellation = ( is_numeric( $blog_id ) && $plugin_id == $this->get_id() && ! $this->is_trial() ); $result = $this->cancel_subscription_or_trial( $plugin_id ); if ( $this->is_api_error( $result ) ) { $this->_admin_notices->add( $result->error->message, $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...', 'error' ); } if ( $switch_to_network_install_blog_after_cancellation ) { $this->switch_to_blog( $this->_storage->network_install_blog_id ); } return; case 'activate_license': check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); $fs = $this; if ( $plugin_id != $this->get_id() ) { $fs = $this->is_addon_activated( $plugin_id ) ? self::get_instance_by_id( $plugin_id ) : null; } if ( is_object( $fs ) ) { $fs->_activate_license(); /** * Remove the product ID from `$_REQUEST` so that the syncing of the license for the other products will work properly. * * @author Leo Fajardo (@leorw) * @since 2.4.0 */ unset( $_REQUEST['plugin_id'] ); if ( $this->is_bundle_license_auto_activation_enabled() ) { $fs->maybe_activate_bundle_license( null, array(), is_numeric( $blog_id ) ? $blog_id : 0 ); } } return; case 'deactivate_license': check_admin_referer( trim( "{$action}:{$blog_id}:{$install_id}", ':' ) ); if ( $plugin_id == $this->get_id() ) { $this->_deactivate_license(); if ( $this->is_only_premium() ) { // Clear user and site. $this->_site = null; $this->_user = null; if ( ! $is_network_action ) { fs_redirect( $this->get_activation_url() ); } else if ( is_numeric( $blog_id ) ) { $this->switch_to_blog( $this->_storage->network_install_blog_id ); } } } else { if ( $this->is_addon_activated( $plugin_id ) ) { $fs_addon = self::get_instance_by_id( $plugin_id ); $fs_addon->_deactivate_license(); } } return; case 'check_updates': check_admin_referer( $action ); $this->check_updates(); return; case 'change_owner': $state = fs_request_get( 'state', 'init' ); switch ( $state ) { case 'init': // 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, $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' ), '<b>' . $this->_user->email . '</b>' ) ); } 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' ), '<b>' . $this->_user->email . '</b>' ) ); } } 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' ), '<b>' . $candidate_email . '</b>' ) ); 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' ), '<b>' . $this->_user->email . '</b>' ), 'ownership_changed', $this->get_text_x_inline( 'Congrats', 'as congratulations', 'congrats' ) . '!' ); } else { // @todo Handle failed ownership change message. } break; } return; case 'update_user_name': check_admin_referer( 'update_user_name' ); $result = $this->update_user_name(); if ( isset( $result->error ) ) { $this->_admin_notices->add( $this->get_text_inline( 'Please provide your full name.', 'name-update-failed-message' ), $oops_text, 'error' ); } else { $this->_admin_notices->add( $this->get_text_inline( 'Your name was successfully updated.', 'name-updated-message' ) ); } return; #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 ) ) { $this->_admin_notices->add( $result->error->message, $this->get_text_x_inline( 'Oops', 'exclamation', 'oops' ) . '...', 'error' ); } return; case 'verify_email': $this->verify_email(); return; case 'sync_user': $this->_handle_account_user_sync(); return; case $this->get_unique_affix() . '_sync_license': $this->_sync_license(); return; case 'download_latest': $this->download_latest_directly( $plugin_id ); return; #endregion } if ( WP_FS__IS_POST_REQUEST ) { $properties = array( 'site_secret_key', 'site_id', 'site_public_key' ); foreach ( $properties as $p ) { if ( 'update_' . $p === $action ) { check_admin_referer( $action ); $this->_logger->log( $action ); $site_property = substr( $p, strlen( 'site_' ) ); $site_property_value = fs_request_get( 'fs_' . $p . '_' . $this->get_unique_affix(), '' ); $this->get_site()->{$site_property} = $site_property_value; // Store account after modification. $this->_store_site(); $this->do_action( 'account_property_edit', 'site', $site_property, $site_property_value ); $this->_admin_notices->add( sprintf( /* translators: %s: User's account property (e.g. email address, name) */ $this->get_text_inline( 'You have successfully updated your %s.', 'x-updated' ), '<b>' . str_replace( '_', ' ', $p ) . '</b>' ) ); return; } } } } /** * Adds CSS classes for the body tag in the admin. * * @param string $classes Space-separated string of class names. * * @return string $classes FS Admin body tag class names. */ public function fs_addons_body_class( $classes ) { $classes .= ' plugins-php'; return $classes; } /** * Account page resources load. * * @author Vova Feldman (@svovaf) * @since 1.0.6 */ function _account_page_load() { $this->_logger->entrance(); $this->_logger->info( var_export( $_REQUEST, true ) ); fs_enqueue_local_style( 'fs_account', '/admin/account.css' ); if ( $this->has_addons() ) { wp_enqueue_script( 'plugin-install' ); add_thickbox(); add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) ); } if ( $this->has_paid_plan() && ! $this->has_any_license() && ! $this->is_sync_executed() && $this->is_tracking_allowed() ) { /** * If no licenses found and no sync job was executed during the last 24 hours, * just execute the sync job right away (blocking execution). * * @since 1.1.7.3 */ $this->run_manual_sync(); } $this->_handle_account_edits(); if ( is_object( $this->_license ) && $this->_license->user_id == $this->_user->id && ! $this->is_whitelabeled( true ) ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( "Is this your client's site? %s if you wish to hide sensitive info like your email, license key, prices, billing address & invoices from the WP Admin.", 'license_not_whitelabeled' ), sprintf( '<a href="#" class="fs-toggle-whitelabel-mode">%s</a>', $this->get_text_inline( 'Click here', 'click-here' ) ) ), '', 'success', false, 'license_not_whitelabeled' ); } $this->do_action( 'account_page_load_before_departure' ); } /** * Renders the "Affiliation" page. * * @author Leo Fajardo (@leorw) * @since 1.2.3 */ function _affiliation_page_render() { $this->_logger->entrance(); $this->fetch_affiliate_and_terms(); fs_enqueue_local_style( 'fs_affiliation', '/admin/affiliation.css' ); $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 ) ); } /** * Render account page. * * @author Vova Feldman (@svovaf) * @since 1.0.0 */ function _account_page_render() { $this->_logger->entrance(); $template = 'account.php'; $vars = array( 'id' => $this->_module_id ); /** * Added filter to the template to allow developers wrapping the template * in custom HTML (e.g. within a wizard/tabs). * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 */ echo $this->apply_filters( "templates/{$template}", fs_get_template( $template, $vars ) ); } /** * Render account connect page. * * @author Vova Feldman (@svovaf) * @since 1.0.7 */ function _connect_page_render() { $this->_logger->entrance(); $vars = array( 'id' => $this->_module_id ); /** * Added filter to the template to allow developers wrapping the template * in custom HTML (e.g. within a wizard/tabs). * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 */ echo $this->apply_filters( 'templates/connect.php', fs_get_template( 'connect.php', $vars ) ); } /** * Load required resources before add-ons page render. * * @author Vova Feldman (@svovaf) * @since 1.0.6 */ function _addons_page_load() { $this->_logger->entrance(); fs_enqueue_local_style( 'fs_addons', '/admin/add-ons.css' ); wp_enqueue_script( 'plugin-install' ); add_thickbox(); add_filter( 'admin_body_class', array( $this, 'fs_addons_body_class' ) ); if ( ! $this->is_registered() && $this->is_org_repo_compliant() ) { $this->_admin_notices->add( sprintf( $this->get_text_inline( 'Just letting you know that the add-ons information of %s is being pulled from an external server.', 'addons-info-external-message' ), '<b>' . $this->get_plugin_name() . '</b>' ), $this->get_text_x_inline( 'Heads up', 'advance notice of something that will need attention.', 'heads-up' ), 'update-nag' ); } } /** * Render add-ons page. * * @author Vova Feldman (@svovaf) * @since 1.0.6 */ function _addons_page_render() { $this->_logger->entrance(); $vars = array( 'id' => $this->_module_id ); /** * Added filter to the template to allow developers wrapping the template * in custom HTML (e.g. within a wizard/tabs). * * @author Vova Feldman (@svovaf) * @since 1.2.1.6 */ echo $this->apply_filters( 'templates/add-ons.php', fs_get_template( 'add-ons.php', $vars ) ); } /* Pricing & Upgrade ------------------------------------------------------------------------------------------------------------------*/ /** * Render pricing page. * * @author Vova Feldman (@svovaf) * @since 1.0.0 */ function _pricing_page_render() { $this->_logger->entrance(); $vars = array( 'id' => $this->_module_id ); if ( 'true' === fs_request_get( 'checkout', false ) ) { echo $this->apply_filters( 'templates/checkout.php', fs_get_template( 'checkout.php', $vars ) ); } else { echo $this->apply_filters( 'templates/pricing.php', fs_get_template( 'pricing.php', $vars ) ); } } /** * @author Leo Fajardo (@leorw) * @since 2.3.1 */ function _maybe_add_pricing_ajax_handler() { if ( ! $this->should_use_external_pricing() ) { $this->add_ajax_action( 'pricing_ajax_action', array( &$this, '_fs_pricing_ajax_action_handler' ) ); } } /** * @author Leo Fajardo (@leorw) * @since 2.3.1 */ function _fs_pricing_ajax_action_handler() { $this->check_ajax_referer( 'pricing_ajax_action' ); $result = null; $pricing_action = fs_request_get( 'pricing_action' ); switch ( $pricing_action ) { case 'fetch_pricing_data': $params = array( 'is_enriched' => true, 'trial' => fs_request_get_bool( 'trial' ), '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(); $bundle_public_key = $this->get_bundle_public_key(); $has_bundle_context = ( FS_Plugin::is_valid_id( $bundle_id ) && ! empty( $bundle_public_key ) ); if ( ! $has_bundle_context ) { $api = $this->get_api_plugin_scope(); } else { $api = FS_Api::instance( $bundle_id, 'plugin', $bundle_id, $bundle_public_key, ! $this->is_live(), false, $this->get_sdk_version() ); $params['plugin_id'] = $this->get_id(); $params['plugin_public_key'] = $this->get_public_key(); } $result = $api->get( 'pricing.json?' . http_build_query( $params ) ); break; case 'start_trial': $result = $this->opt_in( false, false, false, false, false, fs_request_get( 'plan_id' ) ); } if ( is_object( $result ) && $this->is_api_error( $result ) ) { $this->_logger->api_error( $result ); self::shoot_ajax_failure( isset( $result->error ) ? ( is_string( $result->error ) ? $result->error : $result->error->message ) : var_export( $result, true ) ); } $this->shoot_ajax_success( $result ); } #---------------------------------------------------------------------------------- #region Contact Us #---------------------------------------------------------------------------------- /** * Render contact-us page. * * @author Vova Feldman (@svovaf) * @since 1.0.3 */ function _contact_page_render() { $this->_logger->entrance(); $vars = array( 'id' => $this->_module_id ); /** * Added filter to the template to allow developers wrapping the template * in custom HTML (e.g. within a wizard/tabs). * * @author Vova Feldman (@svovaf) * @since 2.1.3 */ echo $this->apply_filters( 'templates/contact.php', fs_get_template( 'contact.php', $vars ) ); } #endregion ------------------------------------------------------------------------ /** * Hide all admin notices to prevent distractions. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * * @uses remove_all_actions() */ private static function _hide_admin_notices() { remove_all_actions( 'admin_notices' ); remove_all_actions( 'network_admin_notices' ); remove_all_actions( 'all_admin_notices' ); remove_all_actions( 'user_admin_notices' ); } static function _clean_admin_content_section_hook() { $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 '<style>#wpfooter { display: none !important; }</style>'; } /** * Attach to admin_head hook to hide all admin notices. * * @author Vova Feldman (@svovaf) * @since 1.0.3 */ static function _clean_admin_content_section() { add_action( 'admin_head', 'Freemius::_clean_admin_content_section_hook' ); } /* CSS & JavaScript ------------------------------------------------------------------------------------------------------------------*/ /* function _enqueue_script($handle, $src) { $url = plugins_url( substr( WP_FS__DIR_JS, strlen( $this->_plugin_dir_path ) ) . '/assets/js/' . $src ); $this->_logger->entrance( 'script = ' . $url ); wp_enqueue_script( $handle, $url ); }*/ /* SDK ------------------------------------------------------------------------------------------------------------------*/ private $_user_api; /** * * @author Vova Feldman (@svovaf) * @since 1.0.2 * * @param bool $flush * * @return FS_Api */ 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 ); } return $this->_user_api; } /** * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @param \FS_User $user * * @return \FS_Api */ private function get_api_user_scope_by_user( FS_User $user ) { return FS_Api::instance( $this->_module_id, 'user', $user->id, $user->public_key, ! $this->is_live(), $user->secret_key, $this->get_sdk_version() ); } /** * * @author Leo Fajardo (@leorw) * @since 2.0.0 * * @param bool $flush * * @return FS_Api */ private function get_current_or_network_user_api_scope( $flush = false ) { if ( ! $this->_is_network_active || ( isset( $this->_user ) && $this->_user instanceof FS_User ) ) { return $this->get_api_user_scope( $flush ); } $user = $this->get_current_or_network_user(); $this->_user_api = FS_Api::instance( $this->_module_id, 'user', $user->id, $user->public_key, ! $this->is_live(), $user->secret_key, $this->get_sdk_version() ); return $this->_user_api; } private $_site_api; /** * * @author Vova Feldman (@svovaf) * @since 1.0.2 * * @param bool $flush * * @return FS_Api */ private function get_api_site_scope( $flush = false ) { if ( ! isset( $this->_site_api ) || $flush ) { $this->_site_api = FS_Api::instance( $this->_module_id, 'install', $this->_site->id, $this->_site->public_key, ! $this->is_live(), $this->_site->secret_key, $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; /** * Get plugin public API scope. * * @author Vova Feldman (@svovaf) * @since 1.0.7 * * @return FS_Api */ function get_api_plugin_scope() { if ( ! isset( $this->_plugin_api ) ) { $this->_plugin_api = FS_Api::instance( $this->_module_id, 'plugin', $this->_plugin->id, $this->_plugin->public_key, ! $this->is_live(), false, $this->get_sdk_version() ); } return $this->_plugin_api; } /** * Get bundle public API scope. * * @author Vova Feldman (@svovaf) * @since 2.3.1 * * @return FS_Api */ function get_api_bundle_scope() { return FS_Api::instance( $this->get_bundle_id(), 'plugin', $this->get_bundle_id(), $this->get_bundle_public_key(), ! $this->is_live(), false, $this->get_sdk_version() ); } /** * Get site API scope object (fallback to public plugin scope when not registered). * * @author Vova Feldman (@svovaf) * @since 1.0.7 * * @return FS_Api */ function get_api_site_or_plugin_scope() { return $this->is_registered() ? $this->get_api_site_scope() : $this->get_api_plugin_scope(); } /** * @author Leo Fajardo (@leorw) * @since 2.2.3.1 * * @param object $result */ private function maybe_modify_api_curl_error_message( $result ) { if ( 'cUrlMissing' !== $result->error->type && ( 'CurlException' !== $result->error->type || CURLE_COULDNT_CONNECT != $result->error->code ) && ( 'HttpRequestFailed' !== $result->error->type || false === strpos( $result->error->message, 'cURL error ' . CURLE_COULDNT_CONNECT ) ) ) { return; } $result->error->message = $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' ) . ' ' . $this->esc_html_inline( sprintf( 'Please contact your hosting provider and ask them to whitelist %s for external connection.', implode( ', ', $this->apply_filters( 'api_domains', array( 'api.freemius.com', 'wp.freemius.com' ) ) ) ), 'connectivity-whitelist' ) . ' ' . sprintf( $this->esc_html_inline( 'Once you are done, deactivate the %s and activate it again.', 'connectivity-reactivate-module' ), $this->get_module_type() ); } /** * Show trial promotional notice (if any trial exist). * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @param FS_Plugin_Plan[] $plans */ function _check_for_trial_plans( $plans ) { /** * For some reason core's do_action() flattens arrays when it has a single object item. Therefore, we need to restructure the array as expected. * * @author Vova Feldman (@svovaf) * @since 2.1.2 */ if ( ! is_array( $plans ) && is_object( $plans ) ) { $plans = array( $plans ); } if ( ! $this->is_array_instanceof( $plans, 'FS_Plugin_Plan' ) ) { $plans = array(); } $this->_storage->has_trial_plan = FS_Plan_Manager::instance()->has_trial_plan( $plans ); } /** * During trial promotion the "upgrade" submenu item turns to * "start trial" to encourage the trial. Since we want to keep * the same menu item handler and there's no robust way to * add new arguments to the menu item link's querystring, * use JavaScript to find the menu item and update the href of * the link. * * @author Vova Feldman (@svovaf) * @since 1.2.1.5 */ function _fix_start_trial_menu_item_url() { $template_args = array( 'id' => $this->_module_id ); fs_require_template( 'add-trial-to-pricing.php', $template_args ); } /** * Check if module is currently in a trial promotion mode. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return bool */ function is_in_trial_promotion() { return $this->_admin_notices->has_sticky( 'trial_promotion' ); } /** * Show trial promotional notice (if any trial exist). * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool If trial notice added. */ function _add_trial_notice() { if ( ! $this->is_user_admin() ) { return false; } if ( ! $this->is_user_in_admin() ) { return false; } if ( $this->_is_network_active ) { if ( fs_is_network_admin() ) { // Network level trial is disabled at the moment. return false; } if ( ! $this->is_delegated_connection() ) { // Only delegated sites should support trials. return false; } } // Check if trial message is already shown. if ( $this->is_in_trial_promotion() ) { add_action( 'admin_footer', array( &$this, '_fix_start_trial_menu_item_url' ) ); $this->_menu->add_counter_to_menu_item( 1, 'fs-trial' ); return false; } if ( $this->is_premium() && ! WP_FS__DEV_MODE ) { // Don't show trial if running the premium code, unless running in DEV mode. return false; } if ( ! $this->has_trial_plan() ) { // No plans with trial. return false; } if ( ! $this->apply_filters( 'show_trial', true ) ) { // Developer explicitly asked not to show the trial promo. return false; } if ( $this->is_registered() ) { // Check if trial already utilized. if ( $this->_site->is_trial_utilized() ) { return false; } if ( $this->is_paying_or_trial() ) { // Don't show trial if paying or already in trial. return false; } } if ( $this->is_activation_mode() || $this->is_pending_activation() ) { // If not yet opted-in/skipped, or pending activation, don't show trial. return false; } $last_time_trial_promotion_shown = $this->_storage->get( 'trial_promotion_shown', false ); $was_promotion_shown_before = ( false !== $last_time_trial_promotion_shown ); // Show promotion if never shown before and 24 hours after initial activation with FS. if ( ! $was_promotion_shown_before && $this->_storage->install_timestamp > ( time() - $this->apply_filters( 'show_first_trial_after_n_sec', WP_FS__TIME_24_HOURS_IN_SEC ) ) ) { return false; } // OR if promotion was shown before, try showing it every 30 days. if ( $was_promotion_shown_before && $this->apply_filters( 'reshow_trial_after_every_n_sec', 30 * WP_FS__TIME_24_HOURS_IN_SEC ) > time() - $last_time_trial_promotion_shown ) { return false; } $trial_period = $this->_trial_days; $require_payment = $this->_is_trial_require_payment; $trial_url = $this->get_trial_url(); $plans_string = strtolower( $this->get_text_inline( 'Awesome', 'awesome' ) ); if ( $this->is_registered() ) { // If opted-in, override trial with up to date data from API. $trial_plans = FS_Plan_Manager::instance()->get_trial_plans( $this->_plans ); $trial_plans_count = count( $trial_plans ); if ( 0 === $trial_plans_count ) { // If there's no plans with a trial just exit. return false; } /** * @var FS_Plugin_Plan $paid_plan */ $paid_plan = $trial_plans[0]; $require_payment = $paid_plan->is_require_subscription; $trial_period = $paid_plan->trial_period; $total_paid_plans = count( $this->_plans ) - ( FS_Plan_Manager::instance()->has_free_plan( $this->_plans ) ? 1 : 0 ); if ( $total_paid_plans !== $trial_plans_count ) { // Not all paid plans have a trial - generate a string of those that have it. for ( $i = 0; $i < $trial_plans_count; $i ++ ) { $plans_string .= sprintf( ' <a href="%s">%s</a>', $trial_url, $trial_plans[ $i ]->title ); if ( $i < $trial_plans_count - 2 ) { $plans_string .= ', '; } else if ( $i == $trial_plans_count - 2 ) { $plans_string .= ' and '; } } } } $message = sprintf( $this->get_text_x_inline( 'Hey', 'exclamation', 'hey' ) . '! ' . $this->get_text_inline( 'How do you like %s so far? Test all our %s premium features with a %d-day free trial.', 'trial-x-promotion-message' ), sprintf( '<b>%s</b>', $this->get_plugin_name() ), $plans_string, $trial_period ); // "No Credit-Card Required" or "No Commitment for N Days". $cc_string = $require_payment ? sprintf( $this->get_text_inline( 'No commitment for %s days - cancel anytime!', 'no-commitment-for-x-days' ), $trial_period ) : $this->get_text_inline( 'No credit card required', 'no-cc-required' ) . '!'; // Start trial button. $button = ' ' . sprintf( '<a style="margin-left: 10px; vertical-align: super;" href="%s"><button class="button button-primary">%s ➜</button></a>', $trial_url, $this->get_text_x_inline( 'Start free trial', 'call to action', 'start-free-trial' ) ); $this->_admin_notices->add_sticky( $this->apply_filters( 'trial_promotion_message', "{$message} {$cc_string} {$button}" ), 'trial_promotion', '', 'promotion' ); $this->_storage->trial_promotion_shown = WP_FS__SCRIPT_START_TIME; return true; } /** * Lets users/customers know that the product has an affiliate program. * * @author Leo Fajardo (@leorw) * @since 1.2.2.11 * * @return bool Returns true if the notice has been added. */ function _add_affiliate_program_notice() { if ( ! $this->is_user_admin() ) { return false; } if ( ! $this->is_user_in_admin() ) { return false; } // Check if the notice is already shown. if ( $this->_admin_notices->has_sticky( 'affiliate_program' ) ) { return false; } if ( // Product has no affiliate program. ! $this->has_affiliate_program() || // User has applied for an affiliate account. ! empty( $this->_storage->affiliate_application_data ) ) { return false; } if ( ! $this->apply_filters( 'show_affiliate_program_notice', true ) ) { // Developer explicitly asked not to show the notice about the affiliate program. return false; } if ( $this->is_activation_mode() || $this->is_pending_activation() ) { // If not yet opted in/skipped, or pending activation, don't show the notice. return false; } $last_time_notice_was_shown = $this->_storage->get( 'affiliate_program_notice_shown', false ); $was_notice_shown_before = ( false !== $last_time_notice_was_shown ); /** * Do not show the notice if it was already shown before or less than 30 days have passed since the initial * activation with FS. */ if ( $was_notice_shown_before || $this->_storage->install_timestamp > ( time() - ( WP_FS__TIME_24_HOURS_IN_SEC * 30 ) ) ) { return false; } if ( ! $this->is_paying() && FS_Plugin::AFFILIATE_MODERATION_CUSTOMERS == $this->_plugin->affiliate_moderation ) { // If the user is not a customer and the affiliate program is only for customers, don't show the notice. return false; } $message = sprintf( $this->get_text_inline( 'Hey there, did you know that %s has an affiliate program? If you like the %s you can become our ambassador and earn some cash!', 'become-an-ambassador-admin-notice' ), sprintf( '<strong>%s</strong>', $this->get_plugin_name() ), $this->get_module_label( true ) ); // HTML code for the "Learn more..." button. $button = ' ' . sprintf( '<a style="display: block; margin-top: 10px;" href="%s"><button class="button button-primary">%s ➜</button></a>', $this->_get_admin_page_url( 'affiliation' ), $this->get_text_inline( 'Learn more', 'learn-more' ) . '...' ); $this->_admin_notices->add_sticky( $this->apply_filters( 'affiliate_program_notice', "{$message} {$button}" ), 'affiliate_program', '', 'promotion' ); $this->_storage->affiliate_program_notice_shown = WP_FS__SCRIPT_START_TIME; return true; } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.5 */ function _enqueue_common_css() { if ( $this->has_paid_plan() && ! $this->is_paying() ) { // Add basic CSS for admin-notices and menu-item colors. fs_enqueue_local_style( 'fs_common', '/admin/common.css' ); } } /** * @author Leo Fajardo (@leorw) * @since 1.2.2 */ function _show_theme_activation_optin_dialog() { fs_enqueue_local_style( 'fs_connect', '/admin/connect.css' ); add_action( 'admin_footer', array( &$this, '_add_fs_theme_activation_dialog' ) ); } /** * @author Leo Fajardo (@leorw) * @since 1.2.2 */ function _add_fs_theme_activation_dialog() { global $pagenow; if ( 'themes.php' !== $pagenow ) { return; } $vars = array( 'id' => $this->_module_id ); fs_require_once_template( 'connect.php', $vars ); } /* Action Links ------------------------------------------------------------------------------------------------------------------*/ private $_action_links_hooked = false; private $_action_links = array(); /** * Hook to plugin action links filter. * * @author Vova Feldman (@svovaf) * @since 1.0.0 */ private function hook_plugin_action_links() { $this->_logger->entrance(); $this->_action_links_hooked = true; $this->_logger->log( 'Adding action links hooks.' ); // Add action link to settings page. add_filter( 'plugin_action_links_' . $this->_plugin_basename, array( &$this, '_modify_plugin_action_links_hook' ), WP_FS__DEFAULT_PRIORITY, 2 ); add_filter( 'network_admin_plugin_action_links_' . $this->_plugin_basename, array( &$this, '_modify_plugin_action_links_hook' ), WP_FS__DEFAULT_PRIORITY, 2 ); } /** * Add plugin action link. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param $label * @param $url * @param bool $external * @param int $priority * @param bool $key */ function add_plugin_action_link( $label, $url, $external = false, $priority = WP_FS__DEFAULT_PRIORITY, $key = false ) { $this->_logger->entrance(); if ( ! isset( $this->_action_links[ $priority ] ) ) { $this->_action_links[ $priority ] = array(); } if ( false === $key ) { $key = preg_replace( "/[^A-Za-z0-9 ]/", '', strtolower( $label ) ); } $this->_action_links[ $priority ][] = array( 'label' => $label, 'href' => $url, 'key' => $key, 'external' => $external ); } /** * Adds Upgrade and Add-Ons links to the main Plugins page link actions collection. * * @author Vova Feldman (@svovaf) * @since 1.0.0 */ function _add_upgrade_action_link() { $this->_logger->entrance(); $is_activation_mode = $this->is_activation_mode(); $add_action_links = $this->should_add_submenu_or_action_links( $is_activation_mode ); /** * The following logic is based on the logic in `add_submenu_items()` method that decides when the "Upgrade" * and "Add-Ons" menus should be added. * * @author Leo Fajardo (@leorw) * @since 2.3.0 */ $add_upgrade_link = ( $add_action_links || ( $is_activation_mode && $this->is_only_premium() ) ) && ! WP_FS__DEMO_MODE && ( ! $this->is_whitelabeled() ); $add_addons_link = ( $add_action_links && $this->has_addons() ); if ( ! $add_upgrade_link && ! $add_addons_link ) { return; } if ( $add_upgrade_link && $this->is_pricing_page_visible() && $this->is_submenu_item_visible( 'pricing' ) ) { $this->add_plugin_action_link( $this->get_text_inline( 'Upgrade', 'upgrade' ), $this->get_upgrade_url(), false, 7, 'upgrade' ); } if ( $add_addons_link && $this->has_addons() && $this->is_submenu_item_visible( 'addons' ) ) { $this->add_plugin_action_link( $this->get_text_inline( 'Add-Ons', 'add-ons' ), $this->_get_admin_page_url( 'addons' ), false, 9, 'addons' ); } } /** * Adds "Activate License" or "Change License" link to the main Plugins page link actions collection. * * @author Leo Fajardo (@leorw) * @since 1.1.9 */ function _add_license_action_link() { $this->_logger->entrance(); if ( ! self::is_ajax() ) { // Inject license activation dialog UI and client side code. add_action( 'admin_footer', array( &$this, '_add_license_activation_dialog_box' ) ); } $link_text = $this->is_free_plan() ? $this->get_text_inline( 'Activate License', 'activate-license' ) : $this->get_text_inline( 'Change License', 'change-license' ); $this->add_plugin_action_link( $link_text, '#', false, 11, ( 'activate-license ' . $this->get_unique_affix() ) ); } /** * @author Leo Fajardo (@leorw) * @since 2.0.2 */ function _add_premium_version_upgrade_selection_action() { $this->_logger->entrance(); if ( ! self::is_ajax() ) { add_action( 'admin_footer', array( &$this, '_add_premium_version_upgrade_selection_dialog_box' ) ); } } /** * Adds "Opt In" or "Opt Out" link to the main "Plugins" page link actions collection. * * @author Leo Fajardo (@leorw) * @since 1.2.1.5 */ function _add_tracking_links() { if ( ! current_user_can( 'manage_options' ) ) { return; } $this->_logger->entrance(); 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; } if ( $this->is_addon() && ! $this->is_only_premium() ) { $parent = $this->get_parent_instance(); if ( is_object( $parent ) && $parent->is_anonymous() ) { return; } } if ( fs_is_network_admin() ) { if ( ! $this->_is_network_active ) { // Don't add tracking links when browsing the network WP Admin and the plugin is not network active. return; } else if ( $this->is_network_delegated_connection() ) { // Don't add tracking links when browsing the network WP Admin and the activation has been delegated to site admins. return; } } else { if ( $this->_is_network_active && ! $this->is_delegated_connection() ) { // Don't add tracking links when browsing the sub-site WP Admin, the plugin is network active, and the connection was not delegated. return; } } if ( fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' ) ) { if ( ! $this->is_registered() && $this->is_anonymous() ) { $this->connect_again(); return; } } if ( ( $this->is_plugin() && ! self::is_plugins_page() ) || ( $this->is_theme() && ! self::is_themes_page() ) ) { // Only show tracking links on the plugins and themes pages. return; } if ( $this->is_activation_mode() && $this->is_premium() && ! $this->is_registered() ) { // If not yet registered and running the premium code base, a license activation link will already be shown. return; } if ( $this->is_registered() && $this->is_tracking_allowed() ) { if ( ! $this->is_premium() && ! $this->is_enable_anonymous() ) { // If opted in and tracking is allowed, don't allow to opt out if not premium and anonymous mode is disabled. return; } } if ( $this->add_ajax_action( 'toggle_permission_tracking', array( &$this, '_toggle_permission_tracking_callback' ) ) ) { return; } $link_text_id = ''; $url = '#'; 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' ); } } else if ( $this->is_anonymous() || $this->is_activation_mode() ) { /** * Show opt-in link only if skipped or in activation mode. */ $link_text_id = $this->get_text_inline( 'Opt In', 'opt-in' ); $params = ! $this->is_anonymous() ? array() : array( 'nonce' => wp_create_nonce( $this->get_unique_affix() . '_reconnect' ), 'fs_action' => ( $this->get_unique_affix() . '_reconnect' ), ); $url = $this->get_activation_url( $params ); } add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) ); if ( ! empty( $link_text_id ) && $this->is_plugin() && self::is_plugins_page() ) { $this->add_plugin_action_link( $link_text_id, $url, false, 13, "opt-in-or-opt-out {$this->_slug}" ); } } /** * Get the URL of the page that should be loaded right after the plugin activation. * * @author Vova Feldman (@svovaf) * @since 1.1.7.4 * * @return string */ function get_after_plugin_activation_redirect_url() { $url = false; if ( ! $this->is_addon() || ! $this->has_free_plan() ) { $first_time_path = $this->_menu->get_first_time_path( fs_is_network_admin() && $this->_is_network_active ); if ( $this->is_activation_mode() ) { $url = $this->get_activation_url(); } else if ( ! empty( $first_time_path ) ) { $url = $first_time_path; } else { $page = ''; if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) { if ( $this->is_network_registered() ) { $page = 'account'; } else if ( $this->is_pending_activation() || $this->is_network_anonymous() ) { $this->maybe_set_slug_and_network_menu_exists_flag(); } } $url = $this->_get_admin_page_url( $page ); } } else { $plugin_fs = false; if ( $this->is_parent_plugin_installed() ) { $plugin_fs = self::get_parent_instance(); } if ( is_object( $plugin_fs ) ) { if ( ! $plugin_fs->is_registered() ) { // Forward to parent plugin connect when parent not registered. $url = $plugin_fs->get_activation_url(); } else { // Forward to account page. $url = $plugin_fs->_get_admin_page_url( 'account' ); } } } return $url; } /** * Forward page to activation page. * * @author Vova Feldman (@svovaf) * @since 1.0.3 */ function _redirect_on_activation_hook() { if ( $this->apply_filters( 'redirect_on_activation', true ) ) { $url = $this->get_after_plugin_activation_redirect_url(); if ( is_string( $url ) ) { fs_redirect( $url ); } } } /** * Modify plugin's page action links collection. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param array $links * @param $file * * @return array */ function _modify_plugin_action_links_hook( $links, $file ) { $this->_logger->entrance(); $passed_deactivate = false; $deactivate_link = ''; $before_deactivate = array(); $after_deactivate = array(); foreach ( $links as $key => $link ) { if ( 'deactivate' === $key ) { $deactivate_link = $link; $passed_deactivate = true; continue; } if ( ! $passed_deactivate ) { $before_deactivate[ $key ] = $link; } else { $after_deactivate[ $key ] = $link; } } ksort( $this->_action_links ); foreach ( $this->_action_links as $new_links ) { foreach ( $new_links as $link ) { $before_deactivate[ $link['key'] ] = '<a href="' . $link['href'] . '"' . ( $link['external'] ? ' target="_blank" rel="noopener"' : '' ) . '>' . $link['label'] . '</a>'; } } if ( ! empty( $deactivate_link ) ) { /** * This HTML element is used to identify the correct plugin when attaching an event to its Deactivate link. * * @since 1.2.1.6 Always show the deactivation feedback form since we added automatic free version deactivation upon premium code activation. */ $deactivate_link .= '<i class="fs-module-id" data-module-id="' . $this->_module_id . '"></i>'; // Append deactivation link. $before_deactivate['deactivate'] = $deactivate_link; } return array_merge( $before_deactivate, $after_deactivate ); } /** * Adds admin message. * * @author Vova Feldman (@svovaf) * @since 1.0.4 * * @param string $message * @param string $title * @param string $type */ function add_admin_message( $message, $title = '', $type = 'success' ) { $this->_admin_notices->add( $message, $title, $type ); } /** * Adds sticky admin message. * * @author Vova Feldman (@svovaf) * @since 1.1.0 * * @param string $message * @param string $id * @param string $title * @param string $type */ function add_sticky_admin_message( $message, $id, $title = '', $type = 'success' ) { $this->_admin_notices->add_sticky( $message, $id, $title, $type ); } /** * Check if the paid version of the module is installed. * * @author Vova Feldman (@svovaf) * @since 2.2.0 * * @return bool */ private function is_premium_version_installed() { $premium_plugin_basename = $this->premium_plugin_basename(); 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 ) ); } /** * Helper function that returns the final steps for the upgrade completion. * * If the module is already running the premium code, returns an empty string. * * @author Vova Feldman (@svovaf) * @since 1.2.1 * * @param string $plan_title * * @return string */ private function get_complete_upgrade_instructions( $plan_title = '' ) { $this->_logger->entrance(); $activate_license_string = $this->get_license_network_activation_notice(); if ( ! $this->has_premium_version() || $this->is_premium() ) { return '' . $activate_license_string; } if ( empty( $plan_title ) ) { $plan_title = $this->get_plan_title(); } if ( $this->is_premium_version_installed() ) { /** * If the premium version is already installed, instead of showing the installation instructions, * tell the current user to activate it. * * @author Leo Fajardo (@leorw) * @since 2.2.1 */ $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 */ $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ), sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ), $plan_title, sprintf( '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>', ( $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' ), $plan_title ) ) ) ); } else { // @since 1.2.1.5 The free version is auto deactivated. $deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ? ( '<li>' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.</li>' ) : ''; return sprintf( ' %s: <ol><li>%s.</li>%s<li>%s (<a href="%s" target="_blank" rel="noopener">%s</a>).</li></ol>', $this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ), ( empty( $activate_license_string ) ? '' : $activate_license_string . '</li><li>' ) . $this->get_latest_download_link( sprintf( /* translators: %s: Plan title */ $this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ), $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/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 * * @param string $url * @param array $request */ private static function enrich_request_for_debug( &$url, &$request ) { if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) { $url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url ); $url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url ); $request['cookies'] = array( new WP_Http_Cookie( array( 'name' => 'XDEBUG_SESSION', 'value' => 'PHPSTORM', ) ) ); } } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 * * @param string $url * @param array $request * @param int $success_cache_expiration * @param int $failure_cache_expiration * @param bool $maybe_enrich_request_for_debug * * @return WP_Error|array */ static function safe_remote_post( &$url, $request, $success_cache_expiration = 0, $failure_cache_expiration = 0, $maybe_enrich_request_for_debug = true ) { $should_cache = ($success_cache_expiration + $failure_cache_expiration > 0); $cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false; $response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ? get_transient( $cache_key ) : false; if ( false === $response ) { if ( $maybe_enrich_request_for_debug ) { self::enrich_request_for_debug( $url, $request ); } if ( ! isset( $request['method'] ) ) { $request['method'] = 'POST'; } $response = FS_Api::remote_request( $url, $request ); 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 = FS_Api::remote_request( $url, $request ); } if ( false !== $cache_key ) { set_transient( $cache_key, $response, ( ( $response instanceof WP_Error ) ? $failure_cache_expiration : $success_cache_expiration ) ); } } return $response; } /** * This method is used to enrich the after upgrade notice instructions when the upgraded * license cannot be activated network wide (license quota isn't large enough). * * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @return string */ private function get_license_network_activation_notice() { if ( ! $this->_is_network_active ) { // Module isn't network level activated. return ''; } if ( ! fs_is_network_admin() ) { // Not network level admin. return ''; } if ( get_blog_count() == 1 ) { // There's only a single site in the network so if there's a context license it was already activated. return ''; } if ( ! is_object( $this->_license ) ) { // No context license. return ''; } if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) { // License was already utilized (this is not 100% the case if all the network is localhost sites and the license can be utilized on unlimited localhost sites). return ''; } if ( $this->can_activate_license_on_network( $this->_license ) ) { // License can be activated on all the network, so probably, the license is already activate on all the network (that's how the after upgrade sync works). return ''; } return sprintf( $this->get_text_inline( '%sClick here%s to choose the sites where you\'d like to activate the license on.', 'network-choose-sites-for-license' ), '<a href="' . $this->get_account_url( false, array( 'activate_license' => 'true' ) ) . '">', '</a>' ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.1.7 * * @param string $key * * @return string */ function get_text( $key ) { return fs_text( $key, $this->_slug ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.3 * * @param string $text Translatable string. * @param string $key String key for overrides. * * @return string */ function get_text_inline( $text, $key = '' ) { return _fs_text_inline( $text, $key, $this->_slug ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.3 * * @param string $text Translatable string. * @param string $context Context information for the translators. * @param string $key String key for overrides. * * @return string */ function get_text_x_inline( $text, $context, $key ) { return _fs_text_x_inline( $text, $context, $key, $this->_slug ); } /** * @author Vova Feldman (@svovaf) * @since 1.2.3 * * @param string $text Translatable string. * @param string $key String key for overrides. * * @return string */ function esc_html_inline( $text, $key ) { return esc_html( _fs_text_inline( $text, $key, $this->_slug ) ); } #---------------------------------------------------------------------------------- #region Versioning #---------------------------------------------------------------------------------- /** * Check if Freemius in SDK upgrade mode. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function is_sdk_upgrade_mode() { return isset( $this->_storage->sdk_upgrade_mode ) ? $this->_storage->sdk_upgrade_mode : false; } /** * Turn SDK upgrade mode off. * * @author Vova Feldman (@svovaf) * @since 1.0.9 */ function set_sdk_upgrade_complete() { $this->_storage->sdk_upgrade_mode = false; } /** * Check if plugin upgrade mode. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function is_plugin_upgrade_mode() { return isset( $this->_storage->plugin_upgrade_mode ) ? $this->_storage->plugin_upgrade_mode : false; } /** * Turn plugin upgrade mode off. * * @author Vova Feldman (@svovaf) * @since 1.0.9 */ function set_plugin_upgrade_complete() { $this->_storage->plugin_upgrade_mode = false; $license_migration = ! empty( $this->_storage->license_migration ) ? $this->_storage->license_migration : array(); $license_migration['is_migrating'] = false; $this->_storage->license_migration = $license_migration; } #endregion #---------------------------------------------------------------------------------- #region Permissions #---------------------------------------------------------------------------------- /** * Check if specific permission requested. * * @author Vova Feldman (@svovaf) * @since 1.1.6 * * @param string $permission * * @return bool */ function is_permission_requested( $permission ) { return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] ); } #endregion #---------------------------------------------------------------------------------- #region Auto Activation #---------------------------------------------------------------------------------- /** * Hints the SDK if running an auto-installation. * * @var bool */ private $_isAutoInstall = false; /** * After upgrade callback to install and auto activate a plugin. * This code will only be executed on explicit request from the user, * following the practice Jetpack are using with their theme installations. * * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/ * * @author Vova Feldman (@svovaf) * @since 1.2.1.7 */ function _install_premium_version_ajax_action() { $this->_logger->entrance(); $this->check_ajax_referer( 'install_premium_version' ); if ( ! $this->is_registered() ) { // Not registered. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ), 'code' => 'premium_installed', ) ); } $plugin_id = fs_request_get( 'target_module_id', $this->get_id() ); if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) { // Invalid ID. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ), 'code' => 'invalid_module_id', ) ); } if ( $plugin_id == $this->get_id() ) { if ( $this->is_premium() ) { // Already using the premium code version. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ), 'code' => 'premium_installed', ) ); } if ( ! $this->can_use_premium_code() ) { // Don't have access to the premium code. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ), 'code' => 'invalid_license', ) ); } if ( ! $this->has_release_on_freemius() ) { // Plugin is a serviceware, no premium code version. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ), 'code' => 'premium_version_missing', ) ); } } else { $addon = $this->get_addon( $plugin_id ); if ( ! is_object( $addon ) ) { // Invalid add-on ID. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ), 'code' => 'invalid_module_id', ) ); } if ( $this->is_addon_activated( $plugin_id, true ) ) { // Premium add-on version is already activated. self::shoot_ajax_failure( array( 'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ), 'code' => 'premium_installed', ) ); } } $this->_isAutoInstall = true; // Try to install and activate. $updater = FS_Plugin_Updater::instance( $this ); $result = $updater->install_and_activate_plugin( $plugin_id ); if ( is_array( $result ) && ! empty( $result['message'] ) ) { self::shoot_ajax_failure( array( 'message' => $result['message'], 'code' => $result['code'], ) ); } self::shoot_ajax_success( $result ); } /** * Displays module activation dialog box after a successful upgrade * where the user explicitly requested to auto download and install * the premium version. * * @author Vova Feldman (@svovaf) * @since 1.2.1.7 */ function _add_auto_installation_dialog_box() { $this->_logger->entrance(); if ( ! $this->is_registered() ) { // Not registered. return; } $plugin_id = fs_request_get( 'plugin_id', $this->get_id() ); if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) { // Invalid module ID. return; } if ( $plugin_id == $this->get_id() ) { if ( $this->is_premium() ) { // Already using the premium code version. return; } if ( ! $this->can_use_premium_code() ) { // Don't have access to the premium code. return; } if ( ! $this->has_release_on_freemius() ) { // Plugin is a serviceware, no premium code version. return; } } else { $addon = $this->get_addon( $plugin_id ); if ( ! is_object( $addon ) ) { // Invalid add-on ID. return; } if ( $this->is_addon_activated( $plugin_id, true ) ) { // Premium add-on version is already activated. return; } } $vars = array( 'id' => $this->_module_id, 'target_module_id' => $plugin_id, 'slug' => $this->_slug, ); fs_require_template( 'auto-installation.php', $vars ); } #endregion #-------------------------------------------------------------------------------- #region Tabs Integration #-------------------------------------------------------------------------------- #region Module's Original Tabs /** * Inject a JavaScript logic to capture the theme tabs HTML. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 */ function _tabs_capture() { $this->_logger->entrance(); if ( ! $this->is_product_settings_page() || ! $this->should_page_include_tabs() || ! $this->is_matching_url( $this->main_menu_url() ) ) { return; } $params = array( 'id' => $this->_module_id, ); fs_require_once_template( 'tabs-capture-js.php', $params ); } /** * Cache theme's tabs HTML for a week. The cache will also be set as expired * after version and type (free/premium) changes, in addition to the week period. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 */ function _store_tabs_ajax_action() { $this->_logger->entrance(); $this->check_ajax_referer( 'store_tabs' ); // Init filesystem if not yet initiated. WP_Filesystem(); // Get POST body HTML data. global $wp_filesystem; $tabs_html = $wp_filesystem->get_contents( "php://input" ); if ( is_string( $tabs_html ) ) { $tabs_html = trim( $tabs_html ); } if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) { self::shoot_ajax_failure(); } $this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC ); self::shoot_ajax_success(); } /** * Cache theme's settings page custom styles. The cache will also be set as expired * after version and type (free/premium) changes, in addition to the week period. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 */ function _store_tabs_styles() { $this->_logger->entrance(); if ( ! $this->is_product_settings_page() || ! $this->should_page_include_tabs() || ! $this->is_matching_url( $this->main_menu_url() ) ) { return; } $wp_styles = wp_styles(); $theme_styles_url = get_template_directory_uri(); $stylesheets = array(); foreach ( $wp_styles->queue as $handler ) { if ( fs_starts_with( $handler, 'fs_' ) ) { // Assume that stylesheets that their handler starts with "fs_" belong to the SDK. continue; } /** * @var _WP_Dependency $stylesheet */ $stylesheet = $wp_styles->registered[ $handler ]; if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) { $stylesheets[] = $stylesheet->src; } } if ( ! empty( $stylesheets ) ) { $this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC ); } } /** * Check if module's original settings page has any tabs. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return bool */ private function has_tabs() { return $this->_cache->has( 'tabs' ); } /** * Get module's settings page HTML content, starting * from the beginning of the <div class="wrap"> element, * until the tabs HTML (including). * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return string */ private function get_tabs_html() { $this->_logger->entrance(); return $this->_cache->get( 'tabs' ); } /** * Check if page should include tabs. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return bool */ private function should_page_include_tabs() { if ( ! $this->has_settings_menu() ) { // Don't add tabs if no settings at all. return false; } if ( self::NAVIGATION_TABS !== $this->_navigation ) { // Only add tabs to themes for now. return false; } if ( $this->is_theme() && ! $this->has_paid_plan() && ! $this->has_addons() ) { // Only add tabs to monetizing themes. return false; } if ( ! $this->is_product_settings_page() ) { // Only add tabs if browsing one of the product's setting pages. return false; } if ( $this->is_activation_mode() && $this->is_activation_page() ) { // Don't include tabs in the activation page. return false; } if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) { // Don't add tabs on checkout page, we want to reduce distractions // as much as possible. return false; } return true; } /** * Add the tabs HTML before the setting's page content and * enqueue any required stylesheets. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return bool If tabs were included. */ function _add_tabs_before_content() { $this->_logger->entrance(); if ( ! $this->should_page_include_tabs() ) { return false; } $tabs_html = $this->get_tabs_html(); if ( empty( $tabs_html ) ) { return false; } /** * Enqueue the original stylesheets that are included in the * theme settings page. That way, if the theme settings has * some custom _styled_ content above the tabs UI, this * will make sure that the styling is preserved. */ $stylesheets = $this->_cache->get( 'tabs_stylesheets', array() ); if ( is_array( $stylesheets ) ) { for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) { wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] ); } } // Cut closing </div> tag. echo substr( trim( $tabs_html ), 0, - 6 ); return true; } /** * Add the tabs closing HTML after the setting's page content. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @return bool If tabs closing HTML was included. */ function _add_tabs_after_content() { $this->_logger->entrance(); if ( ! $this->should_page_include_tabs() ) { return false; } echo '</div>'; return true; } #endregion /** * Add in-page JavaScript to inject the Freemius tabs into * the module's setting tabs section. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 */ function _add_freemius_tabs() { $this->_logger->entrance(); if ( ! $this->should_page_include_tabs() ) { return; } $params = array( 'id' => $this->_module_id ); fs_require_once_template( 'tabs.php', $params ); } #endregion #-------------------------------------------------------------------------------- #region Customizer Integration for Themes #-------------------------------------------------------------------------------- /** * @author Vova Feldman (@svovaf) * @since 1.2.2.7 * * @param WP_Customize_Manager $customizer */ function _customizer_register( $customizer ) { $this->_logger->entrance(); if ( $this->is_pricing_page_visible() ) { require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php'; $customizer->add_section( 'freemius_upsell', array( 'title' => '★ ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ), 'priority' => 1, ) ); $customizer->add_setting( 'freemius_upsell', array( 'sanitize_callback' => 'esc_html', ) ); $customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array( 'fs' => $this, 'section' => 'freemius_upsell', 'priority' => 100, ) ) ); } if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) { require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php'; // Main Documentation Link In Customizer Root. $customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array( 'fs' => $this, 'priority' => 1000, ) ) ); } } #endregion /** * If the theme has a paid version, add some custom * styling to the theme's premium version (if exists) * to highlight that it's the premium version of the * same theme, making it easier for identification * after the user upgrades and upload it to the site. * * @author Vova Feldman (@svovaf) * @since 1.2.2.7 */ function _style_premium_theme() { $this->_logger->entrance(); if ( ! self::is_themes_page() ) { // Only include in the themes page. return; } if ( ! $this->has_paid_plan() ) { // Only include if has any paid plans. return; } $params = null; fs_require_once_template( '/js/jquery.content-change.php', $params ); $params = array( 'slug' => $this->_slug, 'id' => $this->_module_id, ); fs_require_template( '/js/style-premium-theme.php', $params ); } /** * This method will return the absolute URL of the module's local icon. * * When you are running your plugin or theme on a **localhost** environment, if the icon * is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and * it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org. * If an icon is found, this method will automatically attempt to download the icon and store it * in /freemius/assets/img/{slug}.{png|jpg|gif|svg}. * * It's important to mention that this method is NOT phoning home since the developer will deploy * the product with the local icon in the assets folder. The download process just simplifies * the process for the developer. * * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @return string */ function get_local_icon_url() { global $fs_active_plugins; /** * @since 1.1.7.5 */ $local_path = $this->apply_filters( 'plugin_icon', false ); if ( is_string( $local_path ) ) { $icons = array( $local_path ); } else { $img_dir = WP_FS__DIR_IMG; // Locate the main assets folder. if ( 1 < count( $fs_active_plugins->plugins ) ) { $plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root( get_stylesheet() ) ); foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) { if ( $data->plugin_path == $this->get_plugin_basename() ) { $img_dir = $plugin_or_theme_img_dir . '/' /** * The basename will be `themes` or the basename of a custom themes directory. * * @author Leo Fajardo (@leorw) * @since 2.2.3 */ . str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path ) . '/assets/img'; break; } } } // Try to locate the icon in the assets folder. $icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) ); if ( ! is_array( $icons ) || 0 === count( $icons ) ) { if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) { $icons = array( fs_normalize_path( $img_dir . '/theme-icon.png' ) ); } else { $icon_found = false; $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" ); if ( ! function_exists( 'get_filesystem_method' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) ); /** * IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO. * * This code will only be executed once during the testing * of the plugin in a local environment. The plugin icon file WILL * already exist in the assets folder when the plugin is deployed to * the repository. */ if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) { // Fetch icon from Freemius. $icon = $this->fetch_remote_icon_url(); // Fetch icon from WordPress.org. if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) { if ( ! function_exists( 'plugins_api' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; } $plugin_information = plugins_api( 'plugin_information', array( 'slug' => $this->_slug, 'fields' => array( 'sections' => false, 'tags' => false, 'icons' => true ) ) ); if ( ! is_wp_error( $plugin_information ) && isset( $plugin_information->icons ) && ! empty( $plugin_information->icons ) ) { /** * Get the smallest icon. * * @author Leo Fajardo (@leorw) * @since 1.2.2 */ $icon = end( $plugin_information->icons ); } } if ( ! empty( $icon ) ) { if ( 0 !== strpos( $icon, 'http' ) ) { $icon = 'http:' . $icon; } /** * Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765". * * @author Leo Fajardo (@leorw) * @since 1.2.2 */ $ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION ); $local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" ); // Try to download the icon. $icon_found = fs_download_image( $icon, $local_path ); } } if ( ! $icon_found ) { // No icons found, fallback to default icon. if ( $have_write_permissions ) { // If have write permissions, copy default icon. copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path ); } else { // If doesn't have write permissions, use default icon path. $local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ); } } $icons = array( $local_path ); } } } $icon_dir = dirname( $icons[0] ); return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir ); } /** * Fetch module's extended info. * * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @return object|mixed */ private function fetch_module_info() { return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC ); } /** * Fetch module's remote icon URL. * * @author Vova Feldman (@svovaf) * @since 2.0.0 * * @return string */ function fetch_remote_icon_url() { $info = $this->fetch_module_info(); return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ? $info->icon : ''; } #-------------------------------------------------------------------------------- #region GDPR #-------------------------------------------------------------------------------- /** * @author Leo Fajardo (@leorw) * @since 2.1.0 * * @param array $user_plugins * * @return string */ private function get_gdpr_admin_notice_string( $user_plugins ) { $this->_logger->entrance(); $addons = self::get_all_addons(); foreach ( $user_plugins as $user_plugin ) { $has_addons = isset( $addons[ $user_plugin->id ] ); if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) { if ( $this->_module_id == $user_plugin->id ) { $addons = $this->get_addons(); $has_addons = ( ! empty( $addons ) ); } else { $plugin_api = FS_Api::instance( $user_plugin->id, 'plugin', $user_plugin->id, $user_plugin->public_key, ! $user_plugin->is_live, false, $this->get_sdk_version() ); $addons_result = $plugin_api->get( '/addons.json?enriched=true', true ); if ( $this->is_api_result_object( $addons_result, 'plugins' ) && is_array( $addons_result->plugins ) && ! empty( $addons_result->plugins ) ) { $has_addons = true; } } } $user_plugin->has_addons = $has_addons; } $is_single_parent_product = ( 1 === count( $user_plugins ) ); $multiple_products_text = ''; if ( $is_single_parent_product ) { $single_parent_product = reset( $user_plugins ); $thank_you = sprintf( "<span data-plugin-id='%d'>%s</span>", $single_parent_product->id, sprintf( $single_parent_product->has_addons ? $this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) : $this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ), sprintf('<b><i>%s</i></b>', $single_parent_product->title) ) ); $already_opted_in = sprintf( $this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving the %s.", 'already-opted-in-to-product-usage-tracking' ), ( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN ); } else { $thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' ); $already_opted_in = $this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving them.", 'already-opted-in-to-products-usage-tracking' ); $products_and_add_ons = ''; foreach ( $user_plugins as $user_plugin ) { if ( ! empty( $products_and_add_ons ) ) { $products_and_add_ons .= ', '; } if ( ! $user_plugin->has_addons ) { $products_and_add_ons .= sprintf( "<span data-plugin-id='%d'>%s</span>", $user_plugin->id, $user_plugin->title ); } else { $products_and_add_ons .= sprintf( "<span data-plugin-id='%d'>%s</span>", $user_plugin->id, sprintf( $this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ), $user_plugin->title ) ); } } $multiple_products_text = sprintf( "<small class='products'><strong>%s:</strong> %s</small>", $this->get_text_inline( 'Products', 'products' ), $products_and_add_ons ); } $actions = sprintf( '<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>', sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ), $this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ), sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ), sprintf( $this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ), '<span class="underlined">', '</span>' ) ); return sprintf( '%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' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) . '<br><br>' . '<b>' . $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' ) . '</b>' . $actions . ( $is_single_parent_product ? '' : $multiple_products_text ) ); } /** * This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the * plugins and themes they've opted in to. * * @author Leo Fajardo (@leorw) * @since 2.1.0 * * @param string $user_email * @param string $license_key * @param array $plugin_ids * @param string|null $license_key * * @return array|false */ private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) { $request = array( 'method' => 'POST', 'body' => array(), 'timeout' => WP_FS__DEBUG_SDK ? 60 : 30, ); if ( is_string( $user_email ) ) { $request['body']['email'] = $user_email; } else { $request['body']['license_key'] = $license_key; } $result = array(); $url = WP_FS__ADDRESS . '/action/service/user_plugin/'; $total_plugin_ids = count( $plugin_ids ); $plugin_ids_count_per_request = 10; for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) { $plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request ); $request['body']['plugin_ids'] = $plugin_ids_set; $response = self::safe_remote_post( $url, $request, WP_FS__TIME_24_HOURS_IN_SEC, WP_FS__TIME_12_HOURS_IN_SEC ); if ( ! is_wp_error( $response ) ) { $decoded = is_string( $response['body'] ) ? json_decode( $response['body'] ) : null; if ( !is_object($decoded) || !isset($decoded->success) || true !== $decoded->success || !isset( $decoded->data ) || !is_array( $decoded->data ) ) { return false; } $result = array_merge( $result, $decoded->data ); } } return $result; } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ function _maybe_show_gdpr_admin_notice() { if ( ! $this->is_user_in_admin() ) { return; } if ( ! $this->should_handle_gdpr_admin_notice() ) { return; } if ( ! $this->is_user_admin() ) { return; } require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php'; $lock = FS_User_Lock::instance(); /** * Try to acquire a 60-sec lock based on the WP user and thread/process ID. */ if ( ! $lock->try_lock( 60 ) ) { return; } /** * @var $current_wp_user WP_User */ $current_wp_user = self::_get_current_wp_user(); /** * @var FS_User $current_fs_user */ $current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email ); $ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC; if ( ! is_object( $current_fs_user ) ) { // 10-year lock. $lock->lock( $ten_years_in_sec ); return; } $gdpr = FS_GDPR_Manager::instance(); if ( $gdpr->is_opt_in_notice_shown() ) { // 30-day lock. $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC ); return; } if ( ! $gdpr->should_show_opt_in_notice() ) { // 10-year lock. $lock->lock( $ten_years_in_sec ); return; } $last_time_notice_shown = $gdpr->last_time_notice_was_shown(); $was_notice_shown_before = ( false !== $last_time_notice_shown ); if ( $was_notice_shown_before && 30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown ) { // If the notice was shown before, show it again after 30 days from the last time it was shown. return; } /** * Find all plugin IDs that were installed by the current admin. */ $plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id ); if ( empty( $plugin_ids_map )) { $lock->lock( $ten_years_in_sec ); return; } $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins( $current_fs_user->email, null, array_keys( $plugin_ids_map ) ); if ( empty( $user_plugins ) ) { $lock->lock( is_array($user_plugins) ? $ten_years_in_sec : // Lock for 24-hours on errors. WP_FS__TIME_24_HOURS_IN_SEC ); return; } $has_unset_marketing_optin = false; foreach ( $user_plugins as $user_plugin ) { if ( true == $user_plugin->is_marketing_allowed ) { unset( $plugin_ids_map[ $user_plugin->plugin_id ] ); } if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) { $has_unset_marketing_optin = true; } } if ( empty( $plugin_ids_map ) || ( $was_notice_shown_before && ! $has_unset_marketing_optin ) ) { $lock->lock( $ten_years_in_sec ); return; } $modules = array_merge( array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ), array_values( self::maybe_get_entities_account_option( 'themes', array() ) ) ); foreach ( $modules as $module ) { if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) { $plugin_ids_map[ $module->id ] = $module; } } $plugin_title = null; if ( 1 === count( $plugin_ids_map ) ) { $module = reset( $plugin_ids_map ); $plugin_title = $module->title; } $gdpr->add_opt_in_sticky_notice( $this->get_gdpr_admin_notice_string( $plugin_ids_map ), $plugin_title ); $this->add_gdpr_optin_ajax_handler_and_style(); $gdpr->notice_was_just_shown(); // 30-day lock. $lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC ); } /** * Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow * marketing. * * @author Leo Fajardo (@leorw) * @since 2.1.0 */ private function disable_opt_in_notice_and_lock_user() { FS_GDPR_Manager::instance()->disable_opt_in_notice(); require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php'; // 10-year lock. 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 */ function _add_gdpr_optin_js() { $vars = array( 'id' => $this->_module_id ); fs_require_once_template( 'gdpr-optin-js.php', $vars ); } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ function enqueue_gdpr_optin_notice_style() { fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' ); } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ function _maybe_add_gdpr_optin_ajax_handler() { $this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) ); if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) { $this->add_gdpr_optin_ajax_handler_and_style(); } } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ function _fetch_is_marketing_required_flag_value_ajax_action() { $this->_logger->entrance(); $this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' ); $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' ) ); } $user_plugins = $this->fetch_user_marketing_flag_status_by_plugins( null, $license_key, array( $this->_module_id ) ); if ( ! is_array( $user_plugins ) || empty($user_plugins) || !isset($user_plugins[0]->plugin_id) || $user_plugins[0]->plugin_id != $this->_module_id ) { /** * If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in. * * @author Vova Feldman (@svovaf) */ self::shoot_ajax_success( array( 'is_marketing_allowed' => null, 'license_owner_id' => null ) ); } self::shoot_ajax_success( array( 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed, 'license_owner_id' => ( isset( $user_plugins[0]->license_owner_id ) ? $user_plugins[0]->license_owner_id : null ) ) ); } /** * @author Leo Fajardo (@leorw) * @since 2.3.2 * * @param number[] $install_ids * * @return array { * An array of objects containing the installs' licenses owners data. * * @property number $id User ID. * @property string $email User email (can be masked email). * } */ private function fetch_installs_licenses_owners_data( $install_ids ) { $this->_logger->entrance(); $response = $this->get_api_user_scope()->get( '/licenses_owners.json?install_ids=' . implode( ',', $install_ids ) ); $license_owners = array(); if ( $this->is_api_result_object( $response, 'owners' ) ) { $license_owners = $response->owners; } return $license_owners; } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ private function add_gdpr_optin_ajax_handler_and_style() { // Add GDPR action AJAX callback. $this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) ); add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) ); add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) ); } /** * @author Leo Fajardo (@leorw) * @since 2.1.0 */ function _gdpr_optin_ajax_action() { $this->_logger->entrance(); $this->check_ajax_referer( 'gdpr_optin_action' ); if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) { self::shoot_ajax_failure(); } $current_wp_user = self::_get_current_wp_user(); $plugin_ids = fs_request_get( 'plugin_ids', array() ); if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) { self::shoot_ajax_failure(); } $modules = array_merge( array_values( self::maybe_get_entities_account_option( 'plugins', array() ) ), array_values( self::maybe_get_entities_account_option( 'themes', array() ) ) ); foreach ( $modules as $key => $module ) { if ( ! in_array( $module->id, $plugin_ids ) ) { unset( $modules[ $key ] ); } } if ( empty( $modules ) ) { self::shoot_ajax_failure(); } $user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) ); foreach ( $modules as $module ) { $user_api->call( "?plugin_id={$module->id}", 'put', array( 'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) ) ) ); } FS_GDPR_Manager::instance()->remove_opt_in_notice(); require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php'; // 10-year lock. FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC ); self::shoot_ajax_success(); } /** * Checks if the GDPR admin notice should be handled. By default, this logic is off, unless the integrator adds the special 'handle_gdpr_admin_notice' filter. * * @author Vova Feldman (@svovaf) * @since 2.1.0 * * @return bool */ private function should_handle_gdpr_admin_notice() { return $this->apply_filters( 'handle_gdpr_admin_notice', // Default to false. false ); } #endregion #---------------------------------------------------------------------------------- #region Marketing #---------------------------------------------------------------------------------- /** * Check if current user purchased any other plugins before. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function has_purchased_before() { // TODO: Implement has_purchased_before() method. throw new Exception( 'not implemented' ); } /** * Check if current user classified as an agency. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function is_agency() { // TODO: Implement is_agency() method. throw new Exception( 'not implemented' ); } /** * Check if current user classified as a developer. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function is_developer() { // TODO: Implement is_developer() method. throw new Exception( 'not implemented' ); } /** * Check if current user classified as a business. * * @author Vova Feldman (@svovaf) * @since 1.0.9 * * @return bool */ function is_business() { // TODO: Implement is_business() method. throw new Exception( 'not implemented' ); } #endregion #---------------------------------------------------------------------------------- #region Helper #---------------------------------------------------------------------------------- /** * If running with a secret key, assume it's the developer and show pending plans as well. * * @author Vova Feldman (@svovaf) * @since 2.1.2 * * @param string $path * * @return string */ function add_show_pending( $path ) { if ( ! $this->has_secret_key() ) { return $path; } return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true'; } #endregion }
•
Search:
•
Replace:
1
2
3
Function
Edit by line
Download
Information
Rename
Copy
Move
Delete
Chmod
List