From 3d6e3716b08a92a3352bd1c4b989a58db5891b13 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Wed, 15 Apr 2026 10:25:15 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feature:=20Add=20github=20repo=20updat?= =?UTF-8?q?e=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Support/class-github-updater.php | 243 +++++++++++++++++++++++++++ src/class-plugin.php | 3 + 2 files changed, 246 insertions(+) create mode 100644 src/Support/class-github-updater.php diff --git a/src/Support/class-github-updater.php b/src/Support/class-github-updater.php new file mode 100644 index 0000000..14c2a1b --- /dev/null +++ b/src/Support/class-github-updater.php @@ -0,0 +1,243 @@ +plugin_file = $plugin_file; + $this->plugin_slug = plugin_basename( $plugin_file ); + + $headers = get_file_data( $plugin_file, array( 'Version' => 'Version' ) ); + $this->current_version = ! empty( $headers['Version'] ) ? $headers['Version'] : '0.0.0'; + } + + /** + * Register WordPress hooks. + * + * @return void + */ + public function register() { + add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'inject_update' ) ); + add_filter( 'plugins_api', array( $this, 'plugin_info' ), 20, 3 ); + add_filter( 'upgrader_process_complete', array( $this, 'purge_transient' ), 10, 2 ); + } + + /** + * Inject update data into WordPress's update_plugins transient. + * + * @param object $transient The update_plugins transient value. + * @return object + */ + public function inject_update( $transient ) { + if ( empty( $transient->checked ) ) { + return $transient; + } + + $release = $this->fetch_latest_release(); + + if ( ! $release ) { + return $transient; + } + + $remote_version = ltrim( $release['tag_name'], 'v' ); + + if ( version_compare( $this->current_version, $remote_version, '>=' ) ) { + return $transient; + } + + $download_url = $this->get_download_url( $release ); + + if ( ! $download_url ) { + return $transient; + } + + $transient->response[ $this->plugin_slug ] = (object) array( + 'slug' => dirname( $this->plugin_slug ), + 'plugin' => $this->plugin_slug, + 'new_version' => $remote_version, + 'url' => 'https://github.com/' . self::GITHUB_USER . '/' . self::GITHUB_REPO, + 'package' => $download_url, + ); + + return $transient; + } + + /** + * Supply plugin info for the "View version details" modal in the admin. + * + * @param false|object|array $result The result object or array, or false. + * @param string $action The type of info being requested. + * @param object $args Plugin API arguments. + * @return false|object + */ + public function plugin_info( $result, $action, $args ) { + if ( 'plugin_information' !== $action ) { + return $result; + } + + if ( ! isset( $args->slug ) || dirname( $this->plugin_slug ) !== $args->slug ) { + return $result; + } + + $release = $this->fetch_latest_release(); + + if ( ! $release ) { + return $result; + } + + $remote_version = ltrim( $release['tag_name'], 'v' ); + $download_url = $this->get_download_url( $release ); + $changelog = ! empty( $release['body'] ) ? nl2br( esc_html( $release['body'] ) ) : ''; + + return (object) array( + 'name' => 'Logo Soup', + 'slug' => dirname( $this->plugin_slug ), + 'version' => $remote_version, + 'author' => 'Vincent Design Inc', + 'homepage' => 'https://github.com/' . self::GITHUB_USER . '/' . self::GITHUB_REPO, + 'download_link' => $download_url, + 'trunk' => $download_url, + 'requires' => '6.1', + 'requires_php' => '7.4', + 'sections' => array( + 'description' => esc_html__( 'Build balanced logo grids and marquees directly inside the WordPress block editor.', 'logo-soup' ), + 'changelog' => $changelog, + ), + ); + } + + /** + * Delete the cached release transient after a successful plugin update. + * + * @param \WP_Upgrader $upgrader Upgrader instance. + * @param array $hook_extra Extra data about the upgrade. + * @return void + */ + public function purge_transient( $upgrader, $hook_extra ) { + if ( + ! empty( $hook_extra['action'] ) && + 'update' === $hook_extra['action'] && + ! empty( $hook_extra['type'] ) && + 'plugin' === $hook_extra['type'] && + ! empty( $hook_extra['plugins'] ) && + in_array( $this->plugin_slug, (array) $hook_extra['plugins'], true ) + ) { + delete_transient( self::TRANSIENT ); + } + } + + /** + * Fetch the latest GitHub release, with a 12-hour transient cache. + * + * @return array|null Decoded release data or null on failure. + */ + private function fetch_latest_release() { + $cached = get_transient( self::TRANSIENT ); + + if ( is_array( $cached ) ) { + return $cached; + } + + $url = sprintf( + 'https://api.github.com/repos/%s/%s/releases/latest', + self::GITHUB_USER, + self::GITHUB_REPO + ); + + $response = wp_remote_get( + $url, + array( + 'timeout' => 10, + 'user-agent' => 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), + 'headers' => array( + 'Accept' => 'application/vnd.github+json', + ), + ) + ); + + if ( is_wp_error( $response ) ) { + return null; + } + + if ( 200 !== 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'] ) ) { + return null; + } + + set_transient( self::TRANSIENT, $data, self::TRANSIENT_TTL ); + + return $data; + } + + /** + * Extract the zip download URL from a release. + * + * Prefers an attached release asset (.zip); falls back to the auto-generated + * GitHub source zipball. + * + * @param array $release Decoded GitHub release data. + * @return string|null + */ + private function get_download_url( array $release ) { + if ( ! empty( $release['assets'] ) && is_array( $release['assets'] ) ) { + foreach ( $release['assets'] as $asset ) { + if ( + isset( $asset['browser_download_url'] ) && + '.zip' === substr( $asset['browser_download_url'], -4 ) + ) { + return $asset['browser_download_url']; + } + } + } + + return isset( $release['zipball_url'] ) ? $release['zipball_url'] : null; + } +} diff --git a/src/class-plugin.php b/src/class-plugin.php index aa046f9..a65b6e4 100644 --- a/src/class-plugin.php +++ b/src/class-plugin.php @@ -10,6 +10,7 @@ namespace LogoSoup; use LogoSoup\Admin\Settings_Page; use LogoSoup\Blocks\Logo_Soup_Block; use LogoSoup\REST\Settings_Controller; +use LogoSoup\Support\GitHub_Updater; /** * Main plugin bootstrap class. @@ -34,6 +35,8 @@ class Plugin { register_activation_hook( $plugin_file, array( Installer::class, 'activate' ) ); register_deactivation_hook( $plugin_file, array( Installer::class, 'deactivate' ) ); + ( new GitHub_Updater( $plugin_file ) )->register(); + add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) ); add_action( 'init', array( __CLASS__, 'register_settings' ) ); add_action( 'init', array( Logo_Soup_Block::class, 'register' ) );