From 4c07787a44d62319759ec8ad4027048aeaf3c785 Mon Sep 17 00:00:00 2001 From: Anthony Correa Date: Thu, 2 Apr 2026 14:44:10 -0500 Subject: [PATCH] Add GitHub releases updater --- includes/sp-github-updater.php | 306 +++++++++++++++++++++++++++++ tonys-sportspress-enhancements.php | 6 + 2 files changed, 312 insertions(+) create mode 100644 includes/sp-github-updater.php diff --git a/includes/sp-github-updater.php b/includes/sp-github-updater.php new file mode 100644 index 0000000..95c9601 --- /dev/null +++ b/includes/sp-github-updater.php @@ -0,0 +1,306 @@ +release_api_url = sprintf( + 'https://api.github.com/repos/%s/releases/latest', + TONY_SPORTSPRESS_ENHANCEMENTS_GITHUB_REPO + ); + $this->plugin_basename = TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME; + $this->plugin_slug = dirname( $this->plugin_basename ); + + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update' ) ); + add_filter( 'plugins_api', array( $this, 'plugin_information' ), 20, 3 ); + add_filter( 'upgrader_source_selection', array( $this, 'normalize_source_directory' ), 10, 4 ); + add_action( 'upgrader_process_complete', array( $this, 'purge_release_cache' ), 10, 2 ); + } + + /** + * Adds plugin update data to WordPress' update transient. + * + * @param stdClass $transient Existing update transient. + * @return stdClass + */ + public function inject_update( $transient ) { + if ( ! is_object( $transient ) || empty( $transient->checked ) ) { + return $transient; + } + + $release = $this->get_latest_release(); + + if ( ! $release ) { + return $transient; + } + + $remote_version = $this->normalize_version( $release['version'] ); + $current_version = $this->normalize_version( TONY_SPORTSPRESS_ENHANCEMENTS_VERSION ); + + if ( version_compare( $remote_version, $current_version, '<=' ) ) { + return $transient; + } + + $transient->response[ $this->plugin_basename ] = (object) array( + 'id' => $release['url'], + 'slug' => $this->plugin_slug, + 'plugin' => $this->plugin_basename, + 'new_version' => $remote_version, + 'url' => $release['url'], + 'package' => $release['package'], + 'tested' => '', + 'requires_php' => '', + 'icons' => array(), + 'banners' => array(), + 'banners_rtl' => array(), + 'translations' => array(), + ); + + return $transient; + } + + /** + * Provides plugin information for the update details modal. + * + * @param false|object|array $result Existing result. + * @param string $action API action. + * @param object $args API args. + * @return false|object|array + */ + public function plugin_information( $result, $action, $args ) { + if ( 'plugin_information' !== $action || empty( $args->slug ) || $this->plugin_slug !== $args->slug ) { + return $result; + } + + $release = $this->get_latest_release(); + + if ( ! $release ) { + return $result; + } + + return (object) array( + 'name' => 'Tonys SportsPress Enhancements', + 'slug' => $this->plugin_slug, + 'version' => $this->normalize_version( $release['version'] ), + 'author' => 'Tony Correa', + 'author_profile'=> 'https://github.com/anthonyscorrea/', + 'homepage' => $release['url'], + 'download_link' => $release['package'], + 'sections' => array( + 'description' => wp_kses_post( wpautop( 'Suite of SportsPress Enhancements.' ) ), + 'changelog' => wp_kses_post( wpautop( $release['body'] ) ), + ), + ); + } + + /** + * Ensures GitHub's extracted directory name matches the installed plugin slug. + * + * @param string $source Source file location. + * @param string $remote_source Remote file source location. + * @param WP_Upgrader $upgrader Upgrader instance. + * @param array $hook_extra Extra hook arguments. + * @return string|WP_Error + */ + public function normalize_source_directory( $source, $remote_source, $upgrader, $hook_extra ) { + global $wp_filesystem; + + if ( empty( $hook_extra['plugin'] ) || $this->plugin_basename !== $hook_extra['plugin'] ) { + return $source; + } + + $expected_dir = trailingslashit( $remote_source ) . $this->plugin_slug; + + if ( untrailingslashit( $source ) === untrailingslashit( $expected_dir ) ) { + return $source; + } + + if ( ! $wp_filesystem ) { + return $source; + } + + if ( $wp_filesystem->exists( $expected_dir ) ) { + $wp_filesystem->delete( $expected_dir, true ); + } + + if ( ! $wp_filesystem->move( $source, $expected_dir ) ) { + return new WP_Error( + 'tony_sportspress_updater_rename_failed', + __( 'The plugin update package could not be prepared for installation.', 'tonys-sportspress-enhancements' ) + ); + } + + return $expected_dir; + } + + /** + * Clears cached release metadata after plugin updates complete. + * + * @param WP_Upgrader $upgrader Upgrader instance. + * @param array $hook_extra Extra hook arguments. + * @return void + */ + public function purge_release_cache( $upgrader, $hook_extra ) { + if ( empty( $hook_extra['type'] ) || 'plugin' !== $hook_extra['type'] ) { + return; + } + + if ( empty( $hook_extra['plugins'] ) || ! in_array( $this->plugin_basename, (array) $hook_extra['plugins'], true ) ) { + return; + } + + delete_site_transient( $this->cache_key ); + } + + /** + * Reads and caches the latest GitHub release metadata. + * + * @return array|null + */ + private function get_latest_release() { + $cached = get_site_transient( $this->cache_key ); + + if ( is_array( $cached ) ) { + return $cached; + } + + $response = wp_remote_get( + $this->release_api_url, + array( + 'timeout' => 15, + 'headers' => array( + 'Accept' => 'application/vnd.github+json', + 'User-Agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . home_url( '/' ), + ), + ) + ); + + if ( is_wp_error( $response ) ) { + return null; + } + + if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { + return null; + } + + $data = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( ! is_array( $data ) || empty( $data['tag_name'] ) || empty( $data['html_url'] ) ) { + return null; + } + + $release = array( + 'version' => $data['tag_name'], + 'url' => $data['html_url'], + 'body' => isset( $data['body'] ) ? (string) $data['body'] : '', + 'package' => $this->determine_package_url( $data ), + ); + + if ( empty( $release['package'] ) ) { + return null; + } + + set_site_transient( $this->cache_key, $release, 6 * HOUR_IN_SECONDS ); + + return $release; + } + + /** + * Selects the best package URL from a release payload. + * + * @param array $release GitHub release payload. + * @return string + */ + private function determine_package_url( $release ) { + if ( ! empty( $release['assets'] ) && is_array( $release['assets'] ) ) { + $fallback_asset = ''; + + foreach ( $release['assets'] as $asset ) { + if ( empty( $asset['browser_download_url'] ) || empty( $asset['name'] ) ) { + continue; + } + + if ( '.zip' !== strtolower( substr( $asset['name'], -4 ) ) ) { + continue; + } + + if ( false !== strpos( $asset['name'], $this->plugin_slug ) ) { + return $asset['browser_download_url']; + } + + if ( empty( $fallback_asset ) ) { + $fallback_asset = $asset['browser_download_url']; + } + } + + if ( ! empty( $fallback_asset ) ) { + return $fallback_asset; + } + } + + if ( ! empty( $release['zipball_url'] ) ) { + return $release['zipball_url']; + } + + return ''; + } + + /** + * Normalizes release versions so Git tags like v1.2.3 compare correctly. + * + * @param string $version Version string. + * @return string + */ + private function normalize_version( $version ) { + return ltrim( (string) $version, "vV \t\n\r\0\x0B" ); + } + } + + new Tony_Sportspress_GitHub_Updater(); +} diff --git a/tonys-sportspress-enhancements.php b/tonys-sportspress-enhancements.php index 1a5f909..ac1a6a0 100644 --- a/tonys-sportspress-enhancements.php +++ b/tonys-sportspress-enhancements.php @@ -7,6 +7,7 @@ * Author URI: https://github.com/anthonyscorrea/ * Text Domain: tonys-sportspress-enhancements * Domain Path: /languages + * Update URI: https://github.com/anthonyscorrea/tonys-sportspress-enhancements * Version: 0.1.7 * * @package Tonys_Sportspress_Enhancements @@ -28,7 +29,12 @@ if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_URL' ) ) { define( 'TONY_SPORTSPRESS_ENHANCEMENTS_URL', plugin_dir_url( __FILE__ ) ); } +if ( ! defined( 'TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME' ) ) { + define( 'TONY_SPORTSPRESS_ENHANCEMENTS_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); +} + // Include other files here +require_once plugin_dir_path(__FILE__) . 'includes/sp-github-updater.php'; require_once plugin_dir_path(__FILE__) . 'includes/sp-officials-manager-role.php'; require_once plugin_dir_path(__FILE__) . 'includes/open-graph-tags.php'; require_once plugin_dir_path(__FILE__) . 'includes/featured-image-generator.php';