diff --git a/.changeset/witty-falcons-type.md b/.changeset/witty-falcons-type.md new file mode 100644 index 00000000..b86ea2a5 --- /dev/null +++ b/.changeset/witty-falcons-type.md @@ -0,0 +1,6 @@ +--- +"@wpengine/wp-graphql-content-blocks": minor +--- + +Replaced old plugin service to use the WPE updater service for checking for updates. The new API endpoint will be https://wpe-plugin-updates.wpengine.com/wp-graphql-content-blocks/info.json + diff --git a/includes/PluginUpdater/UpdateCallbacks.php b/includes/PluginUpdater/UpdateCallbacks.php deleted file mode 100644 index 1aed0449..00000000 --- a/includes/PluginUpdater/UpdateCallbacks.php +++ /dev/null @@ -1,183 +0,0 @@ -requires_at_least ) || empty( $response->version ) ) { - return $data; - } - - $response->slug = 'wp-graphql-content-blocks'; - $current_plugin_data = \get_plugin_data( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ); - $meets_wp_req = version_compare( get_bloginfo( 'version' ), $response->requires_at_least, '>=' ); - - // Only update the response if there's a newer version, otherwise WP shows an update notice for the same version. - if ( $meets_wp_req && version_compare( $current_plugin_data['Version'], $response->version, '<' ) ) { - $response->plugin = plugin_basename( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ); - $data->response[ WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH ] = $response; - } - - return $data; -} - -add_filter( 'plugins_api', __NAMESPACE__ . '\custom_plugin_api_request', 10, 3 ); -/** - * Callback for WordPress 'plugins_api' filter. - * - * Return a custom response for this plugin from the custom endpoint. - * - * @link https://developer.wordpress.org/reference/hooks/plugins_api/ - * - * @param false|object|array $api The result object or array. Default false. - * @param string $action The type of information being requested from the Plugin Installation API. - * @param object $args Plugin API arguments. - * - * @return false|object|array $response Plugin API arguments. - */ -function custom_plugin_api_request( $api, $action, $args ) { - // Bail if it's not our plugin. - $plugin_slug = dirname( plugin_basename( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ) ); - - if ( empty( $args->slug ) || $plugin_slug !== $args->slug ) { - return $api; - } - - $response = get_plugin_data_from_wpe( $args ); - if ( empty( $response ) || is_wp_error( $response ) ) { - return $api; - } - - return $response; -} - -add_action( 'admin_notices', __NAMESPACE__ . '\delegate_plugin_row_notice' ); -/** - * Callback for WordPress 'admin_notices' action. - * - * Delegate actions to display an error message on the plugin table row if present. - * - * @link https://developer.wordpress.org/reference/hooks/admin_notices/ - * - * @return void - */ -function delegate_plugin_row_notice() { - $screen = get_current_screen(); - if ( ! isset( $screen->id ) || 'plugins' !== $screen->id ) { - return; - } - - $error = get_plugin_api_error(); - if ( ! $error ) { - return; - } - - $plugin_basename = plugin_basename( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ); - - remove_action( "after_plugin_row_{$plugin_basename}", 'wp_plugin_update_row' ); - add_action( "after_plugin_row_{$plugin_basename}", __NAMESPACE__ . '\display_plugin_row_notice', 10 ); -} - -/** - * Callback for WordPress 'after_plugin_row_{plugin_basename}' action. - * - * Callback added in add_plugin_page_notices(). - * - * Show a notice in the plugin table row when there is an error present. - * - * @return void - */ -function display_plugin_row_notice() { - $error = get_plugin_api_error(); - - ?> - - -
-

- -

-
- - - id ) || 'update-core' !== $screen->id ) { - return; - } - - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only used to avoid displaying messages when inappropriate. - if ( ! empty( $_GET['action'] ) && 'do-theme-upgrade' === $_GET['action'] ) { - return; - } - - $error = get_plugin_api_error(); - if ( ! $error ) { - return; - } - - ?> -
-

- -

-
-
' . __( 'THIS UPDATE MAY CONTAIN BREAKING CHANGES: This plugin uses Semantic Versioning, and this new version is a major release. Please review the changelog before updating.', 'wp-graphql-content-blocks' ); -} diff --git a/includes/PluginUpdater/UpdateFunctions.php b/includes/PluginUpdater/UpdateFunctions.php deleted file mode 100644 index 3f554182..00000000 --- a/includes/PluginUpdater/UpdateFunctions.php +++ /dev/null @@ -1,154 +0,0 @@ -requires_at_least, '>=' ); - - $api = new stdClass(); - $api->author = 'WP Engine'; - $api->homepage = 'https://wpengine.com'; - $api->name = $product_info->name; - $api->requires = isset( $product_info->requires_at_least ) ? $product_info->requires_at_least : $current_plugin_data['RequiresWP']; - $api->sections['changelog'] = isset( $product_info->sections->changelog ) ? $product_info->sections->changelog : '

1.0

'; - $api->slug = 'wp-graphql-content-blocks'; - - // Only pass along the update info if the requirements are met and there's actually a newer version. - if ( $meets_wp_req && version_compare( $current_plugin_data['Version'], $product_info->version, '<' ) ) { - $api->version = $product_info->version; - $api->download_link = $product_info->download_link; - } - - return $api; -} - -/** - * Fetches and returns the plugin info api error. - * - * @return mixed|false The plugin api error or false. - */ -function get_plugin_api_error() { - return get_option( 'wpgraphql_content_blocks_product_info_api_error', false ); -} - -/** - * Retrieve remote plugin information from the custom endpoint. - * - * @return \stdClass - */ -function get_remote_plugin_info() { - $current_plugin_data = \get_plugin_data( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ); - $response = get_transient( 'wpgraphql_content_blocks_product_info' ); - - if ( false === $response ) { - $request_args = [ - 'timeout' => ( ( defined( 'DOING_CRON' ) && DOING_CRON ) ? 30 : 3 ), - 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), - 'body' => [ - 'version' => $current_plugin_data['Version'], - ], - ]; - - $response = request_plugin_updates( $request_args ); - if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { - if ( is_wp_error( $response ) ) { - update_option( 'wpgraphql_content_blocks_product_info_api_error', $response->get_error_code(), false ); - } else { - $response_body = json_decode( wp_remote_retrieve_body( $response ), false ); - $error_code = ! empty( $response_body->error_code ) ? $response_body->error_code : 'unknown'; - update_option( 'wpgraphql_content_blocks_product_info_api_error', $error_code, false ); - } - - $response = new stdClass(); - - set_transient( 'wpgraphql_content_blocks_product_info', $response, MINUTE_IN_SECONDS * 5 ); - - return $response; - } - - delete_option( 'wpgraphql_content_blocks_product_info_api_error' ); - - $response = json_decode( - wp_remote_retrieve_body( $response ) - ); - - if ( ! property_exists( $response, 'icons' ) || empty( $response->icons['default'] ) ) { - $plugin_url = plugin_dir_url( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ); - - $response->icons['default'] = $plugin_url . 'includes/updates/images/wpe-logo-stacked-inverse.svg'; - } - - set_transient( 'wpgraphql_content_blocks_product_info', $response, HOUR_IN_SECONDS * 12 ); - } - - return $response; -} - -/** - * Get the remote plugin api error message. - * - * @param string $reason The reason/error code received the API. - * - * @return string The error message. - */ -function get_api_error_text( string $reason ): string { - switch ( $reason ) { - case 'key-unknown': - return __( 'The product you requested information for is unknown. Please contact support.', 'wp-graphql-content-blocks' ); - - default: - return sprintf( - /* translators: %1$s: Link to GitHub issues. %2$s: The text that is linked. */ - __( - 'WPGraphQL Content Blocks encountered an unknown error connecting to the update service. This issue could be temporary. Please %2$s if this error persists.', - 'wp-graphql-content-blocks' - ), - 'https://github.com/wpengine/wp-graphql-content-blocks/issues', - esc_html__( 'contact support', 'wp-graphql-content-blocks' ) - ); - } -} - -/** - * Retrieve plugin update information via http GET request. - * - * @param array $args Array of request args. - * - * @return array|\WP_Error A response as an array or WP_Error. - * @uses wp_remote_get() - * @link https://developer.wordpress.org/reference/functions/wp_remote_get/ - */ -function request_plugin_updates( array $args = [] ) { - return wp_remote_get( - 'https://wp-product-info.wpesvc.net/v1/plugins/wpgraphql-content-blocks', - $args - ); -} diff --git a/includes/Updates/CheckForUpgrades.php b/includes/Updates/CheckForUpgrades.php new file mode 100644 index 00000000..3b501824 --- /dev/null +++ b/includes/Updates/CheckForUpgrades.php @@ -0,0 +1,19 @@ + 'wp-graphql-content-blocks', + 'plugin_basename' => WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH, + ); + + require_once __DIR__ . '/PluginUpdater.php'; + new PluginUpdater( $properties ); + } + + add_action( 'admin_init', __NAMESPACE__ . '\wp_graphql_content_blocks_check_for_upgrades' ); +} \ No newline at end of file diff --git a/includes/Updates/PluginUpdater.php b/includes/Updates/PluginUpdater.php new file mode 100644 index 00000000..e3f013ba --- /dev/null +++ b/includes/Updates/PluginUpdater.php @@ -0,0 +1,247 @@ +api_url = 'https://wpe-plugin-updates.wpengine.com/'; + + $this->cache_time = time() + HOUR_IN_SECONDS * 5; + + $this->properties = $this->get_full_plugin_properties( $properties, $this->api_url ); + + if ( ! $this->properties ) { + return; + } + + $this->register(); + } + + /** + * Get the full plugin properties, including the directory name, version, basename, and add a transient name. + * + * @param array $properties These properties are passed in when instantiating to identify the plugin and it's update location. + * @param string $api_url The URL where the api is located. + * @return array|null + */ + public function get_full_plugin_properties( $properties, $api_url ) { + $plugins = \get_plugins(); + + // Scan through all plugins installed and find the one which matches this one in question. + foreach ( $plugins as $plugin_basename => $plugin_data ) { + // Match using the passed-in plugin's basename. + if ( $plugin_basename === $properties['plugin_basename'] ) { + // Add the values we need to the properties. + $properties['plugin_dirname'] = dirname( $plugin_basename ); + $properties['plugin_version'] = $plugin_data['Version']; + $properties['plugin_update_transient_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ); + $properties['plugin_update_transient_exp_name'] = 'wpesu-plugin-' . sanitize_title( $properties['plugin_dirname'] ) . '-expiry'; + $properties['plugin_manifest_url'] = trailingslashit( $api_url ) . trailingslashit( $properties['plugin_slug'] ) . 'info.json'; + + return $properties; + } + } + + // No matching plugin was found installed. + return null; + } + + /** + * Register hooks. + * + * @return void + */ + public function register() { + add_filter( 'plugins_api', array( $this, 'filter_plugin_update_info' ), 20, 3 ); + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'filter_plugin_update_transient' ) ); + } + + /** + * Filter the plugin update transient to take over update notifications. + * + * @param ?object $transient_value The value of the `site_transient_update_plugins` transient. + * + * @handles site_transient_update_plugins + * @return object + */ + public function filter_plugin_update_transient( $transient_value ) { + // No update object exists. Return early. + if ( empty( $transient_value ) ) { + return $transient_value; + } + + $result = $this->fetch_plugin_info(); + + if ( false === $result ) { + return $transient_value; + } + + $res = $this->parse_plugin_info( $result ); + + if ( version_compare( $this->properties['plugin_version'], $result->version, '<' ) ) { + $transient_value->response[ $res->plugin ] = $res; + $transient_value->checked[ $res->plugin ] = $result->version; + } else { + $transient_value->no_update[ $res->plugin ] = $res; + } + + return $transient_value; + } + + /** + * Filters the plugin update information. + * + * @param object $res The response to be modified for the plugin in question. + * @param string $action The action in question. + * @param object $args The arguments for the plugin in question. + * + * @handles plugins_api + * @return object + */ + public function filter_plugin_update_info( $res, $action, $args ) { + // Do nothing if this is not about getting plugin information. + if ( 'plugin_information' !== $action ) { + return $res; + } + + // Do nothing if it is not our plugin. + if ( $this->properties['plugin_dirname'] !== $args->slug ) { + return $res; + } + + + $result = $this->fetch_plugin_info(); + + // Do nothing if we don't get the correct response from the server. + if ( false === $result ) { + return $res; + } + + return $this->parse_plugin_info( $result ); + } + + /** + * Fetches the plugin update object from the WP Product Info API. + * + * @return object|false + */ + private function fetch_plugin_info() { + // Fetch cache first. + $expiry = get_option( $this->properties['plugin_update_transient_exp_name'], 0 ); + $response = get_option( $this->properties['plugin_update_transient_name'] ); + + if ( empty( $expiry ) || time() > $expiry || empty( $response ) ) { + $response = wp_remote_get( + $this->properties['plugin_manifest_url'], + array( + 'timeout' => 10, + 'headers' => array( + 'Accept' => 'application/json', + ), + ) + ); + + if ( + is_wp_error( $response ) || + 200 !== wp_remote_retrieve_response_code( $response ) || + empty( wp_remote_retrieve_body( $response ) ) + ) { + return false; + } + + $response = wp_remote_retrieve_body( $response ); + + // Cache the response. + update_option( $this->properties['plugin_update_transient_exp_name'], $this->cache_time, false ); + update_option( $this->properties['plugin_update_transient_name'], $response, false ); + } + + $decoded_response = json_decode( $response ); + + if ( json_last_error() !== JSON_ERROR_NONE ) { + return false; + } + + return $decoded_response; + } + + /** + * Parses the product info response into an object that WordPress would be able to understand. + * + * @param object $response The response object. + * + * @return stdClass + */ + private function parse_plugin_info( $response ) { + + global $wp_version; + + $res = new stdClass(); + $res->name = $response->name; + $res->slug = $response->slug; + $res->version = $response->version; + $res->requires = $response->requires; + $res->download_link = $response->download_link; + $res->trunk = $response->download_link; + $res->new_version = $response->version; + $res->plugin = $this->properties['plugin_basename']; + $res->package = $response->download_link; + + // Plugin information modal and core update table use a strict version comparison, which is weird. + // If we're genuinely not compatible with the point release, use our WP tested up to version. + // otherwise use exact same version as WP to avoid false positive. + $res->tested = 1 === version_compare( substr( $wp_version, 0, 3 ), $response->tested ) + ? $response->tested + : $wp_version; + + $res->sections = array( + 'description' => $response->sections->description, + 'changelog' => $response->sections->changelog, + ); + + return $res; + } +} \ No newline at end of file diff --git a/includes/WPGraphQLContentBlocks.php b/includes/WPGraphQLContentBlocks.php index 554a551f..b34c4578 100644 --- a/includes/WPGraphQLContentBlocks.php +++ b/includes/WPGraphQLContentBlocks.php @@ -98,8 +98,9 @@ static function () { } // Include the updater functions. - require_once WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_DIR . '/includes/PluginUpdater/UpdateFunctions.php'; - require_once WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_DIR . '/includes/PluginUpdater/UpdateCallbacks.php'; + if (file_exists(WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_DIR . '/includes/Updates/CheckForUpgrades.php')) { + require_once WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_DIR . '/includes/Updates/CheckForUpgrades.php'; + } // Bail if the Enforce SemVer class doesn't exist. if ( ! class_exists( 'EnforceSemVer\EnforceSemVer' ) ) { diff --git a/wp-graphql-content-blocks.php b/wp-graphql-content-blocks.php index 0f2c1d02..d4f816cf 100644 --- a/wp-graphql-content-blocks.php +++ b/wp-graphql-content-blocks.php @@ -42,9 +42,18 @@ function wpgraphql_content_blocks_constants(): void { if ( ! defined( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH' ) ) { define( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH', plugin_basename( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_FILE ) ); } + if ( ! defined( 'WPGRAPHQL_CONTENT_BLOCKS_VERSION' ) ) { define( 'WPGRAPHQL_CONTENT_BLOCKS_VERSION', '4.5.0' ); } + + if ( ! defined( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_URL' ) ) { + define( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); + } + + if ( ! defined( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH' ) ) { + define( 'WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_PATH', plugin_basename( WPGRAPHQL_CONTENT_BLOCKS_PLUGIN_URL ) ); + } } }