',
$this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
( empty( $activate_license_string ) ? '' : $activate_license_string . '
' ) .
$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/upload-wp-' . $this->_module_type . 's' ),
$this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
);
}
}
/**
* @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 );
}
$response = wp_remote_post( $url, $request );
if ( $response instanceof WP_Error ) {
if ( 'https://' === substr( $url, 0, 8 ) &&
isset( $response->errors ) &&
isset( $response->errors['http_request_failed'] )
) {
$http_error = strtolower( $response->errors['http_request_failed'][0] );
if ( false !== strpos( $http_error, 'ssl' ) ||
false !== strpos( $http_error, 'curl error 35' )
) {
// Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
$url = 'http://' . substr( $url, 8 );
$request['timeout'] = 15;
$response = wp_remote_post( $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' ),
'',
''
);
}
/**
* @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;
}
#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_theme_settings_page() ||
! $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_theme_settings_page() ||
! $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
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 ( ! $this->is_theme() ) {
// Only add tabs to themes for now.
return false;
}
if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
// Only add tabs to monetizing themes.
return false;
}
if ( ! $this->is_theme_settings_page() ) {
// Only add tabs if browsing one of the theme's setting pages.
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;
}
/**
* 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
tag.
echo substr( trim( $this->get_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 '';
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
*
* @return bool
*/
function fetch_and_store_current_user_gdpr_anonymously() {
$pong = $this->ping( null, true );
if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
return false;
} else {
FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
return $pong->is_gdpr_required;
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @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(
"%s",
$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('%s', $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(
"%s",
$user_plugin->id,
$user_plugin->title
);
} else {
$products_and_add_ons .= sprintf(
"%s",
$user_plugin->id,
sprintf(
$this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
$user_plugin->title
)
);
}
}
$multiple_products_text = sprintf(
"%s: %s",
$this->get_text_inline( 'Products', 'products' ),
$products_and_add_ons
);
}
$actions = sprintf(
'
%s - %s
%s - %s
',
sprintf('', $this->get_text_inline( 'Yes', 'yes' ) ),
$this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
sprintf('', $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' ),
'',
''
)
);
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' ), '', '' ) .
'
' .
'' . $this->get_text_inline( "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:", 'contact-for-updates' ) . '' .
$actions .
( $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::$_accounts->get_option( 'plugins', array() ) ),
array_values( self::$_accounts->get_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.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( '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 ) );
}
self::shoot_ajax_success( array( 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed ) );
}
/**
* @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::$_accounts->get_option( 'plugins', array() ) ),
array_values( self::$_accounts->get_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
}
/* small fix to not show scrollbars, make insert section taller */
.tag-generator-panel .insert-box {
padding-left: 15px;
padding-right: 15px;
height: 90px;
}