* 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; $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 ( 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_registered() && $this->is_tracking_allowed() ) { if ( ! $this->is_enable_anonymous() ) { // If opted in and tracking is allowed, don't allow to opt out if anonymous mode is disabled. return; } if ( ! $this->is_free_plan() ) { // Don't allow to opt out if running in paid plan. return; } } if ( $this->add_ajax_action( 'stop_tracking', array( &$this, '_stop_tracking_callback' ) ) ) { return; } if ( $this->add_ajax_action( 'allow_tracking', array( &$this, '_allow_tracking_callback' ) ) ) { return; } $link_text_id = ''; $url = '#'; if ( $this->is_registered() ) { if ( $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' ); } add_action( 'admin_footer', array( &$this, '_add_optout_dialog' ) ); } 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 ); } 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_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() { $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'] ] = '' . $link['label'] . ''; } } 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 .= ''; // 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(); $premium_plugin = get_plugins( '/' . dirname( $premium_plugin_basename ) ); return ! empty( $premium_plugin ); } /** * 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_plugin_basename = $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( '%s', esc_html( $this->get_plugin_title() ) ), $plan_title, sprintf( '', wp_nonce_url( 'plugins.php?action=activate&plugin=' . $premium_plugin_basename, 'activate-plugin_' . $premium_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', '<' ) ? ( '
  • ' . $this->esc_html_inline( 'Deactivate the free version', 'deactivate-free-version' ) . '.
  • ' ) : ''; return sprintf( ' %s:
    1. %s.
    2. %s
    3. %s (%s).
    ', $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( '', 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; }