'', 'key' => '', 'slug' => '', 'basename' => '', 'version' => '', ) ); // Check if is_plugin_active() function exists. This is required on the front end of the // site, since it is in a file that is normally only loaded in the admin. if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // add if is active plugin (not included in theme). if ( is_plugin_active( $plugin['basename'] ) ) { $this->plugins[ $plugin['basename'] ] = $plugin; } } /** * Returns a registered plugin for the give key and value. * * @since 5.7.2 * * @param string $key The array key to compare. * @param string $value The value to compare against. * @return array|false */ public function get_plugin_by( $key = '', $value = null ) { foreach ( $this->plugins as $plugin ) { if ( $plugin[ $key ] === $value ) { return $plugin; } } return false; } /** * Makes a request to the ACF connect server. * * @since 5.5.10 * * @param string $endpoint The API endpoint. * @param array $body The body to post. * @return (array|string|WP_Error) */ public function request( $endpoint = '', $body = null ) { // Determine URL. $url = "https://connect.advancedcustomfields.com/$endpoint"; // Staging environment. if ( defined( 'ACF_DEV_API' ) && ACF_DEV_API ) { $url = trailingslashit( ACF_DEV_API ) . $endpoint; acf_log( $url, $body ); } // Make request. $raw_response = wp_remote_post( $url, array( 'timeout' => 10, 'body' => $body, ) ); // Handle response error. if ( is_wp_error( $raw_response ) ) { return $raw_response; // Handle http error. } elseif ( wp_remote_retrieve_response_code( $raw_response ) !== 200 ) { return new WP_Error( 'server_error', wp_remote_retrieve_response_message( $raw_response ) ); } // Decode JSON response. $json = json_decode( wp_remote_retrieve_body( $raw_response ), true ); // Allow non json value. if ( $json === null ) { return wp_remote_retrieve_body( $raw_response ); } return $json; } /** * Returns update information for the given plugin id. * * @since 5.5.10 * * @param string $id The plugin id such as 'pro'. * @param boolean $force_check Bypasses cached result. Defaults to false. * @return array|WP_Error */ public function get_plugin_info( $id = '', $force_check = false ) { $transient_name = 'acf_plugin_info_' . $id; // check cache but allow for $force_check override. if ( ! $force_check ) { $transient = get_transient( $transient_name ); if ( $transient !== false ) { return $transient; } } $response = $this->request( 'v2/plugins/get-info?p=' . $id ); // convert string (misc error) to WP_Error object. if ( is_string( $response ) ) { $response = new WP_Error( 'server_error', esc_html( $response ) ); } // allow json to include expiration but force minimum and max for safety. $expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS ); // update transient. set_transient( $transient_name, $response, $expiration ); return $response; } /** * Returns specific data from the 'update-check' response. * * @since 5.7.2 * * @param string $basename The plugin basename. * @param boolean $force_check Bypasses cached result. Defaults to false. * @return array|false */ public function get_plugin_update( $basename = '', $force_check = false ) { // get updates. $updates = $this->get_plugin_updates( $force_check ); // check for and return update. if ( is_array( $updates ) && isset( $updates['plugins'][ $basename ] ) ) { return $updates['plugins'][ $basename ]; } return false; } /** * Checks if an update is available, but can't be updated to. * * @since 6.2.1 * * @param string $basename The plugin basename. * @param boolean $force_check Bypasses cached result. Defaults to false. * @return array|false */ public function get_no_update( $basename = '', $force_check = false ) { // get updates. $updates = $this->get_plugin_updates( $force_check ); // check for and return update. if ( is_array( $updates ) && isset( $updates['no_update'][ $basename ] ) ) { return $updates['no_update'][ $basename ]; } return false; } /** * Checks for plugin updates. * * @since 5.6.9 * @since 5.7.2 Added 'checked' comparison * * @param boolean $force_check Bypasses cached result. Defaults to false. * @return array|WP_Error. */ public function get_plugin_updates( $force_check = false ) { $transient_name = 'acf_plugin_updates'; // Construct array of 'checked' plugins. // Sort by key to avoid detecting change due to "include order". $checked = array(); foreach ( $this->plugins as $basename => $plugin ) { $checked[ $basename ] = $plugin['version']; } ksort( $checked ); // $force_check prevents transient lookup. if ( ! $force_check ) { $transient = get_transient( $transient_name ); // If cached response was found, compare $transient['checked'] against $checked and ignore if they don't match (plugins/versions have changed). if ( is_array( $transient ) ) { $transient_checked = isset( $transient['checked'] ) ? $transient['checked'] : array(); if ( wp_json_encode( $checked ) !== wp_json_encode( $transient_checked ) ) { $transient = false; } } if ( $transient !== false ) { return $transient; } } $post = array( 'plugins' => wp_json_encode( $this->plugins ), 'wp' => wp_json_encode( array( 'wp_name' => get_bloginfo( 'name' ), 'wp_url' => acf_get_home_url(), 'wp_version' => get_bloginfo( 'version' ), 'wp_language' => get_bloginfo( 'language' ), 'wp_timezone' => get_option( 'timezone_string' ), 'wp_multisite' => (int) is_multisite(), 'php_version' => PHP_VERSION, ) ), 'acf' => wp_json_encode( array( 'acf_version' => get_option( 'acf_version' ), 'acf_pro' => acf_is_pro(), 'block_count' => acf_pro_get_registered_block_count(), ) ), ); // Check update from connect. $response = $this->request( 'v2/plugins/update-check', $post ); // Append checked reference. if ( is_array( $response ) ) { $response['checked'] = $checked; if ( isset( $response['license_status'] ) && function_exists( 'acf_pro_update_license_status' ) ) { acf_pro_update_license_status( $response['license_status'] ); unset( $response['license_status'] ); } } // Allow json to include expiration but force minimum and max for safety. $expiration = $this->get_expiration( $response, DAY_IN_SECONDS, MONTH_IN_SECONDS ); // Update transient and return. set_transient( $transient_name, $response, $expiration ); return $response; } /** * This function safely gets the expiration value from a response. * * @since 5.6.9 * * @param mixed $response The response from the server. Default false. * @param integer $min The minimum expiration limit. Default 0. * @param integer $max The maximum expiration limit. Default 0. * @return integer */ public function get_expiration( $response = false, $min = 0, $max = 0 ) { $expiration = 0; // Check possible error conditions. if ( is_wp_error( $response ) || is_string( $response ) ) { return 5 * MINUTE_IN_SECONDS; } // Use the server requested expiration if present. if ( is_array( $response ) && isset( $response['expiration'] ) ) { $expiration = (int) $response['expiration']; } // Use the minimum if neither check matches, or ensure the server expiration isn't lower than our minimum. if ( $expiration < $min ) { return $min; } // Ensure the server expiration isn't higher than our max. if ( $expiration > $max ) { return $max; } return $expiration; } /** * Deletes transients and allows a fresh lookup. * * @since 5.5.10 */ public function refresh_plugins_transient() { delete_site_transient( 'update_plugins' ); delete_transient( 'acf_plugin_updates' ); } /** * Called when WP updates the 'update_plugins' site transient. Used to inject ACF plugin update info. * * @since 5.0.0 * * @param object $transient The current transient value. * @return object $transient The modified transient value. */ public function modify_plugins_transient( $transient ) { // Bail early if no response (error). if ( ! isset( $transient->response ) ) { return $transient; } // Ensure no_update is set for back compat. if ( ! isset( $transient->no_update ) ) { $transient->no_update = array(); } // Force-check (only once). $force_check = ( $this->checked == 0 ) ? ! empty( $_GET['force-check'] ) : false; // phpcs:ignore -- False positive, value not used. // Fetch updates (this filter is called multiple times during a single page load). $updates = $this->get_plugin_updates( $force_check ); // Append ACF pro plugins. if ( is_array( $updates ) ) { if ( ! empty( $updates['plugins'] ) ) { foreach ( $updates['plugins'] as $basename => $update ) { $transient->response[ $basename ] = (object) $update; } } if ( ! empty( $updates['no_update'] ) ) { foreach ( $updates['no_update'] as $basename => $update ) { $transient->no_update[ $basename ] = (object) $update; } } } ++$this->checked; return $transient; } /** * Returns the plugin data visible in the 'View details' popup * * @since 5.0.0 * * @param object $result The current result of plugin data. * @param string $action The action being performed. * @param object $args Data about the plugin being retried. * @return $result */ public function modify_plugin_details( $result, $action = null, $args = null ) { $plugin = false; // Only for 'plugin_information' action. if ( $action !== 'plugin_information' ) { return $result; } // Find plugin via slug. $plugin = $this->get_plugin_by( 'slug', $args->slug ); if ( ! $plugin ) { return $result; } // Get data from connect or cache. $response = $this->get_plugin_info( $plugin['id'] ); // Bail early if no response. if ( ! is_array( $response ) ) { return $result; } // Remove tags (different context). unset( $response['tags'] ); // Convert to object. $response = (object) $response; $sections = array( 'description' => '', 'installation' => '', 'changelog' => '', 'upgrade_notice' => '', ); foreach ( $sections as $k => $v ) { $sections[ $k ] = $response->$k; } $response->sections = $sections; return $response; } } /** * The main function responsible for returning the acf_updates singleton. * Use this function like you would a global variable, except without needing to declare the global. * * Example: * * @since 5.5.12 * * @return ACF_Updates The singleton instance of ACF_Updates. */ function acf_updates() { global $acf_updates; if ( ! isset( $acf_updates ) ) { $acf_updates = new ACF_Updates(); } return $acf_updates; } /** * Alias of acf_updates()->add_plugin(). * * @since 5.5.10 * * @param array $plugin Plugin data array. */ function acf_register_plugin_update( $plugin ) { acf_updates()->add_plugin( $plugin ); } }