_namespace = strtolower( $namespace ); $this->_fs = $freemius; $this->_store_url = $store_url; $this->_product_id = $product_id; $this->_license_accessor = $license_accessor; $this->_is_bundle = $is_bundle; $this->_was_freemius_in_prev_version = $was_freemius_in_prev_version; /** * If no license is set it might be one of the following: * 1. User purchased module directly from Freemius. * 2. User did purchase from store, but has never activated the license on this site. * 3. User got access to the code without ever purchasing. * * In case it's reason #2 or if the license key is wrong, the migration will not work. * Since we do want to support store licenses, hook to Freemius `after_install_failure` * event. That way, if a license activation fails, try activating the license on store * first, and if works, migrate to Freemius right after. */ $this->_fs->add_filter( 'after_install_failure', array( &$this, 'try_migrate_on_activation' ), 10, 2 ); if ( $this->should_try_migrate() ) { if ( $this->has_any_keys() ) { if ( ! defined( 'DOING_AJAX' ) ) { $this->non_blocking_license_migration( $is_blocking ); } } } } /** * The license migration script. * * IMPORTANT: * You should use your own function name, and be sure to replace it throughout this file. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param bool $redirect * * @return bool */ protected function do_license_migration( $redirect = false ) { $result = $this->get_site_migration_data_and_licenses(); $migration_data = $result['data']; $all_licenses = $result['licenses']; $transient_key = 'fs_license_migration_' . $this->_product_id . '_' . md5( implode( '', $all_licenses ) ); $response = get_transient( $transient_key ); if ( false === $response ) { $response = wp_remote_post( $this->get_migration_endpoint(), array( 'timeout' => 60, 'sslverify' => false, 'body' => json_encode( $migration_data ), ) ); // Cache result (15-min). set_transient( $transient_key, $response, 15 * MINUTE_IN_SECONDS ); } $should_migrate_transient = $this->get_should_migrate_transient_key(); // make sure the response came back okay if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { $error_message = $response->get_error_message(); return ( is_wp_error( $response ) && ! empty( $error_message ) ) ? $error_message : __( 'An error occurred, please try again.' ); } else { $response = json_decode( wp_remote_retrieve_body( $response ) ); if ( ! is_object( $response ) || ! isset( $response->success ) || true !== $response->success ) { if ( isset( $response->error ) ) { switch ( $response->error->code ) { case 'empty_license_key': case 'invalid_license_key': case 'license_expired': case 'license_disabled': set_transient( $should_migrate_transient, 'no', WP_FS__TIME_24_HOURS_IN_SEC * 365 ); break; default: // Unexpected error. break; } } else { // Unexpected error. } // Failed to pull account information. return false; } // Delete transient on successful migration. delete_transient( $transient_key ); if ( $this->_was_freemius_in_prev_version && $this->_fs->is_registered() ) { if ( $this->_license_accessor->is_network_migration() ) { $this->_fs->delete_network_account_event(); } else { $this->_fs->delete_account_event(); } } if ( $this->_license_accessor->is_network_migration() ) { $installs = array(); foreach ( $response->data->installs as $install ) { $installs[] = new FS_Site( $install ); } $this->_fs->setup_network_account( new FS_User( $response->data->user ), $installs, $redirect ); } else { $this->_fs->setup_account( new FS_User( $response->data->user ), new FS_Site( $response->data->install ), $redirect ); } // Upon successful migration, store the no-migration flag for 5 years. set_transient( $should_migrate_transient, 'no', WP_FS__TIME_24_HOURS_IN_SEC * 365 * 5 ); return true; } } /** * Initiate a non-blocking HTTP POST request to the same URL * as the current page, with the addition of "fsm_{namespace}_{product_id}" * param in the query string that is set to a unique migration * request identifier, making sure only one request will make * the migration. * * @todo Test 2 threads in parallel and make sure that `add_transient()` works as expected. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @return bool Is successfully spawned the migration request. */ protected function spawn_license_migration() { #region Make sure only one request handles the migration (prevent race condition) // Generate unique md5. $migration_uid = md5( rand() . microtime() ); $loaded_migration_uid = false; /** * Use `add_transient()` instead of `set_transient()` because * we only want that one request will succeed writing this * option to the storage. */ $transient_key = "fsm_{$this->_namespace}_{$this->_product_id}"; if ( $this->add_transient( $transient_key, $migration_uid, MINUTE_IN_SECONDS ) ) { $loaded_migration_uid = $this->get_transient( $transient_key ); } if ( $migration_uid !== $loaded_migration_uid ) { return false; } #endregion $migration_url = add_query_arg( "fsm_{$this->_namespace}_{$this->_product_id}", $migration_uid, $this->get_current_url() ); // Add cookies to trigger request with same user access permissions. $cookies = array(); foreach ( $_COOKIE as $name => $value ) { $cookies[] = new WP_Http_Cookie( array( 'name' => $name, 'value' => $value ) ); } wp_remote_post( $migration_url, array( 'timeout' => 0.01, 'blocking' => false, 'sslverify' => false, 'cookies' => $cookies, ) ); return true; } /** * Run non blocking migration if all of the following (AND condition): * 1. Has API connectivity to api.freemius.com * 2. User isn't yet identified with Freemius. * 3. Freemius is in "activation mode". * 4. It's a plugin version upgrade. * 5. It's the first installation of the context plugin that have Freemius integrated with. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param bool $is_blocking Special argument for testing. When false, will initiate the migration in the same HTTP request. * * @return string|bool */ protected function non_blocking_license_migration( $is_blocking = false ) { if ( ! $this->_fs->has_api_connectivity() ) { // No connectivity to Freemius API, it's up to you what to do. return 'no_connectivity'; } if ( ! $this->_fs->is_premium() ) { // Running the free product version, so don't migrate. return 'free_code_version'; } if ( $this->_fs->is_registered() && $this->_fs->has_any_license() ) { // User already identified by the API and has a license. return 'user_registered_with_license'; } if ( ! $this->_was_freemius_in_prev_version ) { if ( $this->_fs->is_registered() ) { // User already identified by the API. return 'user_registered'; } if ( ! $this->_fs->is_activation_mode() ) { // Plugin isn't in Freemius activation mode. return 'not_in_activation'; } if ( ! $this->_fs->is_plugin_upgrade_mode() ) { // Plugin isn't in plugin upgrade mode. return 'not_in_upgrade'; } if ( ! $this->_fs->is_first_freemius_powered_version() ) { // It's not the 1st version of the plugin that runs with Freemius. return 'freemius_installed_before'; } } $key = "fsm_{$this->_namespace}_{$this->_product_id}"; $migration_uid = $this->get_transient( $key ); $in_migration = ! empty( $_REQUEST[ $key ] ); if ( ! $is_blocking && ! $in_migration ) { // Initiate license migration in a non-blocking request. return $this->spawn_license_migration(); } else { if ( $is_blocking || ( ! empty( $_REQUEST[ $key ] ) && $migration_uid === $_REQUEST[ $key ] && 'POST' === $_SERVER['REQUEST_METHOD'] ) ) { $success = $this->do_license_migration(); if ( $success ) { $this->_fs->set_plugin_upgrade_complete(); return 'success'; } } } return 'failed'; } /** * If installation failed due to license activation on Freemius try to * activate the license on store first, and if successful, migrate the license * with a blocking request. * * This method will only be triggered upon failed module installation. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param object $response Freemius installation request result. * @param array $args Freemius installation request arguments. * * @return object|string */ public function try_migrate_on_activation( $response, $args ) { if ( empty( $args['license_key'] ) || $this->_fs->apply_filters('license_key_maxlength', 32) !== strlen( $args['license_key'] ) ) { // No license key provided (or invalid length), ignore. return $response; } if ( ! $this->_fs->has_api_connectivity() ) { // No connectivity to Freemius API, it's up to you what to do. return $response; } if ( ( is_object( $response->error ) && 'invalid_license_key' === $response->error->code ) || ( is_string( $response->error ) && false !== strpos( strtolower( $response->error ), 'license' ) ) ) { // Set license for migration. if ( $this->_is_bundle ) { $this->_children_license_keys = array( $args['license_key'] ); } else { $this->_license_key = $args['license_key']; } // Try to migrate the license. if ( 'success' === $this->non_blocking_license_migration( true ) ) { /** * If successfully migrated license and got to this point (no redirect), * it means that it's an AJAX installation (opt-in), therefore, * override the response with the after connect URL. */ return $this->_fs->get_after_activation_url( 'after_connect_url' ); } } return $response; } #-------------------------------------------------------------------------------- #region Helper Methods #-------------------------------------------------------------------------------- /** * @author Vova Feldman (@svovaf) * @since 1.0.3 * * @return string */ protected function get_migration_endpoint() { return sprintf( '%s/fs-api/%s/migrate-license.json', $this->_store_url, $this->_namespace ); } /** * Prepare data for migration. * * @author Vova Feldman (@svovaf) * @since 1.1.0 * * @return array */ private function get_site_migration_data_and_licenses() { $is_network_migration = $this->_license_accessor->is_network_migration(); $this->wp_cookie_constants(); $migration_data = $this->_fs->get_opt_in_params( array( // Include the migrating product ID. 'module_id' => $this->_product_id, // Override is_premium flat because it's a paid license migration. 'is_premium' => true, // The plugin is active for sure and not uninstalled. 'is_active' => true, 'is_uninstalled' => false, ), ( $is_network_migration ? true : null ) ); // Clean unnecessary arguments. unset( $migration_data['return_url'] ); unset( $migration_data['account_url'] ); $all_licenses = array(); if ( false === $is_network_migration || $this->_license_accessor->are_licenses_network_identical() ) { if ( $this->_is_bundle ) { $migration_data['children_license_keys'] = $this->get_children_licenses(); $all_licenses = $migration_data['children_license_keys']; } else { $migration_data['license_key'] = $this->get_license(); $all_licenses[] = $migration_data['license_key']; } } else { $blog_ids = $this->get_blog_ids(); $keys_by_blog_id = array(); foreach ( $blog_ids as $blog_id ) { $site_keys = $this->_is_bundle ? $this->get_children_licenses( $blog_id ) : $this->get_license( $blog_id ); if ( empty( $site_keys ) ) { continue; } $keys_by_blog_id[ $blog_id ] = $site_keys; } foreach ( $migration_data['sites'] as $index => &$site ) { if ( ! isset( $keys_by_blog_id[ $site['blog_id'] ] ) ) { unset( $migration_data['sites'][ $index ] ); } $site_keys = $keys_by_blog_id[ $site['blog_id'] ]; if ( is_array( $site_keys ) ) { $site['children_license_keys'] = $site_keys; $all_licenses = array_merge( $all_licenses, $site_keys ); } else { $site['license_key'] = $site_keys; $all_licenses[] = $site_keys; } } // Reorder indexes. $migration_data = array_values( $migration_data ); } return array( 'data' => $migration_data, 'licenses' => $all_licenses, ); } /** * Get a collection of all the license keys for the migration. * We use this to generate a unique transient name to store a helper * value to avoid trying a migration with a keys combination that already * failed before. * * @author Vova Feldman (@svovaf) * @since 1.2.0 * * @return string[] */ private function get_all_migration_licenses() { $is_network_migration = $this->_license_accessor->is_network_migration(); $all_licenses = array(); if ( false === $is_network_migration || $this->_license_accessor->are_licenses_network_identical() ) { if ( $this->_is_bundle ) { $all_licenses = $this->get_children_licenses(); } else { $all_licenses[] = $this->get_license(); } } else { $blog_ids = $this->get_blog_ids(); foreach ( $blog_ids as $blog_id ) { $site_keys = $this->_is_bundle ? $this->get_children_licenses( $blog_id ) : $this->get_license( $blog_id ); if ( empty( $site_keys ) ) { continue; } if ( is_array( $site_keys ) ) { $all_licenses = array_merge( $all_licenses, $site_keys ); } else { $all_licenses[] = $site_keys; } } } return $all_licenses; } /** * Define cookie constants which are required by Freemius::get_opt_in_params() since * it uses wp_get_current_user() which needs the cookie constants set. When a plugin * is network activated the cookie constants are only configured after the network * plugins activation, therefore, if we don't define those constants WP will throw * PHP warnings/notices. * * @author Vova Feldman (@svovaf) * @since 1.1.1 */ private function wp_cookie_constants() { if ( defined( 'LOGGED_IN_COOKIE' ) && ( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) ) ) { return; } /** * Used to guarantee unique hash cookies * * @since 1.5.0 */ if ( ! defined( 'COOKIEHASH' ) ) { $siteurl = get_site_option( 'siteurl' ); if ( $siteurl ) { define( 'COOKIEHASH', md5( $siteurl ) ); } else { define( 'COOKIEHASH', '' ); } } if ( ! defined( 'LOGGED_IN_COOKIE' ) ) { define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH ); } /** * @since 2.5.0 */ if ( ! defined( 'AUTH_COOKIE' ) ) { define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH ); } /** * @since 2.6.0 */ if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) { define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH ); } } /** * Get current request full URL. * * @author Vova Feldman (@svovaf) * @since 1.0.3 * * @return string */ private function get_current_url() { $host = $_SERVER['HTTP_HOST']; $uri = $_SERVER['REQUEST_URI']; $port = $_SERVER['SERVER_PORT']; $port = ( ( ! WP_FS__IS_HTTPS && $port == '80' ) || ( WP_FS__IS_HTTPS && $port == '443' ) ) ? '' : ':' . $port; return ( WP_FS__IS_HTTPS ? 'https' : 'http' ) . "://{$host}{$port}{$uri}"; } /** * Checks if there are any keys set at all. * * @author Vova Feldman (@svovaf) * @since 1.1.0 */ private function has_any_keys() { if ( ! $this->_license_accessor->is_network_migration() ) { return $this->_is_bundle ? $this->_license_accessor->site_has_children_keys() : $this->_license_accessor->site_has_key(); } $blog_ids = $this->get_blog_ids(); foreach ( $blog_ids as $blog_id ) { $site_has_keys = $this->_is_bundle ? $this->_license_accessor->site_has_children_keys( $blog_id ) : $this->_license_accessor->site_has_key( $blog_id ); if ( $site_has_keys ) { return true; } } return false; } /** * @author Vova Feldman (@svovaf) * @since 1.1.0 * * @param int|null $blog_id * * @return string */ private function get_license( $blog_id = null ) { return empty( $this->_license_key ) ? $this->_license_accessor->get( $blog_id ) : $this->_license_key; } /** * @author Vova Feldman (@svovaf) * @since 1.1.0 * * @param int|null $blog_id * * @return string[] */ private function get_children_licenses( $blog_id = null ) { return empty( $this->_children_license_keys ) ? $this->_license_accessor->get_children( $blog_id ) : $this->_children_license_keys; } /** * @var string */ private $_shouldMigrateTransientKey; /** * @author Vova Feldman (@svovaf) * @since 1.1.2 * * @return string * * @uses get_all_migration_licenses() */ private function get_should_migrate_transient_key() { if ( ! isset( $this->_shouldMigrateTransientKey ) ) { $keys = $this->get_all_migration_licenses(); $this->_shouldMigrateTransientKey = 'fs_should_migrate_' . md5( $this->_store_url . ':' . $this->_product_id . implode( ':', $keys ) ); } return $this->_shouldMigrateTransientKey; } /** * Check if should try to migrate or not. * * @author Vova Feldman (@svovaf) * @since 1.1.2 * * @return bool */ private function should_try_migrate() { $key = $this->get_should_migrate_transient_key(); $should_migrate = get_transient( $key ); return ( ! is_string( $should_migrate ) || 'no' !== $should_migrate ); } #endregion #-------------------------------------------------------------------------------- #region Database Transient #-------------------------------------------------------------------------------- /** * Very similar to the WP transient mechanism. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param string $transient * * @return mixed */ private function get_transient( $transient ) { $transient_option = '_fs_transient_' . $transient; $transient_timeout = '_fs_transient_timeout_' . $transient; $timeout = get_option( $transient_timeout ); if ( false !== $timeout && $timeout < time() ) { delete_option( $transient_option ); delete_option( $transient_timeout ); $value = false; } else { $value = get_option( $transient_option ); } return $value; } /** * Not like `set_transient()`, this function will only ADD * a transient if it's not yet exist. * * @author Vova Feldman (@svovaf) * @since 1.0.0 * * @param string $transient * @param mixed $value * @param int $expiration * * @return bool TRUE if successfully added a transient. */ private function add_transient( $transient, $value, $expiration = 0 ) { $transient_option = '_fs_transient_' . $transient; $transient_timeout = '_fs_transient_timeout_' . $transient; $current_value = $this->get_transient( $transient ); if ( false === $current_value ) { $autoload = 'yes'; if ( $expiration ) { $autoload = 'no'; add_option( $transient_timeout, time() + $expiration, '', 'no' ); } return add_option( $transient_option, $value, '', $autoload ); } else { // If expiration is requested, but the transient has no timeout option, // delete, then re-create the timeout. if ( $expiration ) { if ( false === get_option( $transient_timeout ) ) { add_option( $transient_timeout, time() + $expiration, '', 'no' ); } } } return false; } #endregion #-------------------------------------------------------------------------------- #region Multisite Network #-------------------------------------------------------------------------------- /** * @author Vova Feldman (@svovaf) * @since 1.1.0 * * @return int[] */ private function get_blog_ids() { global $wpdb; return $wpdb->get_col( "SELECT blog_id FROM {$wpdb->blogs}" ); } #endregion }