set_license( $license ); $this->set_cache_key(); } /** * @since 3.06 * * @param string|null $license The license key. * * @return void */ private function set_license( $license ) { if ( $license === null ) { $edd_update = $this->get_pro_updater(); if ( $edd_update ) { $license = $edd_update->license; } } $this->license = $license; } /** * @since 3.06 * * @return string */ public function get_license() { return $this->license; } /** * @since 3.06 * * @return void */ protected function set_cache_key() { $this->cache_key = 'frm_addons_l' . ( $this->license ? md5( $this->license ) : '' ); } /** * @since 3.06 * * @return string */ public function get_cache_key() { return $this->cache_key; } /** * Flag the force property as true, so the next API request bypasses cache. * This is used to pull API data for change logs, which are excluded from the cached data. * * @since 6.28 * * @return void */ public function force_api_request() { $this->force = true; } /** * @since 3.06 * * @return array */ public function get_api_info() { $url = $this->api_url(); if ( $this->license ) { $url .= '?l=' . urlencode( base64_encode( $this->license ) ); } if ( $this->force ) { $addons = false; $this->force = false; } else { $addons = $this->get_cached(); } if ( is_array( $addons ) ) { return $addons; } if ( $this->is_running() ) { // If there's no saved cache, we'll need to wait for the current request to finish. return array(); } $this->set_running(); // We need to know the version number to allow different downloads. $agent = 'formidable/' . FrmAppHelper::plugin_version(); if ( class_exists( 'FrmProDb' ) ) { $agent = 'formidable-pro/' . FrmProDb::$plug_version; } $response = wp_remote_get( $url, array( 'timeout' => 25, 'user-agent' => $agent . '; ' . get_bloginfo( 'url' ), ) ); if ( is_array( $response ) && ! is_wp_error( $response ) ) { $addons = $response['body'] ? json_decode( $response['body'], true ) : array(); } if ( ! is_array( $addons ) ) { $addons = array(); } $addons['response_code'] = wp_remote_retrieve_response_code( $response ); foreach ( $addons as $k => $addon ) { if ( ! is_array( $addon ) ) { continue; } if ( isset( $addon['categories'] ) ) { $cats = array_intersect( $this->skip_categories(), $addon['categories'] ); if ( $cats ) { unset( $addons[ $k ] ); continue; } } if ( ! array_key_exists( 'is_new', $addon ) && array_key_exists( 'released', $addon ) ) { $addons[ $k ]['is_new'] = $this->is_new( $addon ); } } $this->set_cached( $addons ); $this->done_running(); return $addons; } /** * Prevent multiple requests from running at the same time. * * @since 6.8.3 * * @return bool */ protected function is_running() { if ( $this->run_as_multisite() ) { return get_site_transient( $this->transient_key() ); } return get_transient( $this->transient_key() ); } /** * @since 6.8.3 * * @return void */ protected function set_running() { $expires = 2 * MINUTE_IN_SECONDS; if ( $this->run_as_multisite() ) { set_site_transient( $this->transient_key(), true, $expires ); return; } set_transient( $this->transient_key(), true, $expires ); } /** * @since 6.8.3 * * @return void */ protected function done_running() { if ( $this->run_as_multisite() ) { delete_site_transient( $this->transient_key() ); } delete_transient( $this->transient_key() ); } /** * Only allow one site in the network to make the api request at a time. * If there is a license for the request, run individually. * * @since 6.8.3 * * @return bool */ protected function run_as_multisite() { return is_multisite() && ! $this->license; } /** * @since 6.8.3 * * @return string */ protected function transient_key() { return strtolower( self::class ) . '_request_lock'; } /** * @since 3.06 * * @return string */ protected function api_url() { if ( ! $this->license ) { // Direct traffic to Cloudflare worker when there is no license. return 'https://plapi.formidableforms.com/list/'; } return 'https://formidableforms.com/wp-json/s11edd/v1/updates/'; } /** * @since 3.06 * * @return string[] */ protected function skip_categories() { return array( 'WordPress Form Templates', 'WordPress Form Style Templates' ); } /** * @since 3.06 * * @param object $license_plugin The FrmAddon object. * @param array $addons * * @return array */ public function get_addon_for_license( $license_plugin, $addons = array() ) { if ( ! $addons ) { $addons = $this->get_api_info(); } $download_id = $license_plugin->download_id; $plugin = array(); if ( ! $download_id && $addons ) { foreach ( $addons as $addon ) { if ( is_array( $addon ) && ! empty( $addon['title'] ) && 0 === strcasecmp( $license_plugin->plugin_name, $addon['title'] ) ) { return $addon; } } } elseif ( isset( $addons[ $download_id ] ) ) { $plugin = $addons[ $download_id ]; } return $plugin; } /** * @since 3.06 * * @return false|object */ public function get_pro_updater() { if ( FrmAppHelper::pro_is_installed() && is_callable( 'FrmProAppHelper::get_updater' ) ) { $updater = FrmProAppHelper::get_updater(); $this->set_license( $updater->license ); return $updater; } return false; } /** * @since 3.06 * * @return array|bool */ protected function get_cached() { $cache = $this->get_cached_option(); if ( ! $cache ) { return false; } $is_expired = empty( $cache['timeout'] ) || time() > $cache['timeout']; if ( ! $is_expired && isset( $cache['version'] ) && $cache['version'] !== FrmAppHelper::plugin_version() ) { $is_expired = true; } // Avoid old cached data, unless we're currently trying to query for new data. // The call to $this->is_running likely triggers a database query, so only call if if we're expired. // (Rather than the other way around, which is less efficient). if ( $is_expired && ! $this->is_running() ) { return false; } return json_decode( $cache['value'], true ); } /** * Get the cache for the network if multisite. * * @since 6.8.3 * * @return mixed */ protected function get_cached_option() { if ( is_multisite() ) { $cached = get_site_option( $this->cache_key ); if ( $cached ) { return $cached; } } return get_option( $this->cache_key ); } /** * @since 3.06 * * @param array $addons * * @return void */ protected function set_cached( $addons ) { $addons = $this->reduce_addon_data_before_caching( $addons ); $data = array( 'timeout' => strtotime( $this->get_cache_timeout( $addons ), time() ), 'value' => wp_json_encode( $addons ), 'version' => FrmAppHelper::plugin_version(), ); if ( is_multisite() ) { update_site_option( $this->cache_key, $data ); } else { // Autoload the license cache because it gets called everywhere. $autoload = str_starts_with( $this->cache_key, 'frm_addons_l' ); update_option( $this->cache_key, $data, $autoload ); } } /** * Remove certain add-on API data that we don't need to cache. * This is to help keep the option data (which is auto-loaded) small. * * @since 6.28 * * @param array $addons * * @return array */ private function reduce_addon_data_before_caching( $addons ) { if ( is_subclass_of( $this, 'FrmFormApi' ) ) { // We only want to modify FrmFormApi. Leave the other APIs alone for now. return $addons; } $reduced_addons = array(); foreach ( $addons as $key => $addon ) { if ( ! is_array( $addon ) ) { $reduced_addons[ $key ] = $addon; continue; } if ( ! $this->should_include_addon_in_cached_data( $addon ) ) { continue; } if ( isset( $addon['changelog'] ) ) { unset( $addon['changelog'], $addon['banners'] ); } $reduced_addons[ $key ] = $addon; } return $reduced_addons; } /** * @since 6.28 * * @param array $addon * * @return bool True if the add-on should be included in cached data. */ private function should_include_addon_in_cached_data( $addon ) { if ( isset( $addon['version'] ) && '' === $addon['version'] ) { // If version is set but blank, the plugin is not actually live. return false; } if ( isset( $addon['categories'] ) ) { if ( isset( $addon['slug'] ) && 'views' === $addon['slug'] ) { // Legacy views has no categories set, but we should still // Include it in cache since it is a valid add-on. return true; } $categories_are_empty = ! $addon['categories'] || $addon['categories'] === array( 'Strategy11' ); if ( $categories_are_empty ) { return false; } } return true; } /** * If the last check was a a rate limit, we'll need to check again sooner. * * @since 6.8.3 * * @param array $addons * * @return string */ protected function get_cache_timeout( $addons ) { if ( isset( $addons['response_code'] ) && 429 === $addons['response_code'] ) { return '+5 minutes'; } return $this->cache_timeout; } /** * @since 3.06 * * @return void */ public function reset_cached() { if ( is_multisite() ) { delete_site_option( $this->cache_key ); } else { delete_option( $this->cache_key ); } $this->done_running(); } /** * @since 3.06 * * @return array */ public function error_for_license() { return $this->license ? $this->get_error_from_response() : array(); } /** * @since 3.06 * * @param array $addons * * @return array */ public function get_error_from_response( $addons = array() ) { if ( ! $addons ) { $addons = $this->get_api_info(); } $errors = array(); if ( ! isset( $addons['error'] ) ) { return $errors; } if ( is_string( $addons['error'] ) ) { $errors[] = $addons['error']; } elseif ( ! empty( $addons['error']['message'] ) ) { $errors[] = $addons['error']['message']; } do_action( 'frm_license_error', $addons['error'] ); return $errors; } /** * Check if a template is new. * * @since 6.0 * * @param array $addon * * @return bool */ protected function is_new( $addon ) { return strtotime( $addon['released'] ) > strtotime( '-' . $this->new_days . ' days' ); } }