! ( is_string( $item ) && str_contains( $item, 'open-plugin-details-modal' ) ) ) ); $meta[] = sprintf( '%s', esc_url( self::REPO_URL . '/releases/tag/v' . USC_VERSION ), esc_html__( 'View details', 'unsupervised-schedular' ) ); return $meta; } /** * `update_plugins_{hostname}` filter callback. * * For a newer release with a zip asset, returns the update array core * files under the transient's `response` list (the update offer). * Otherwise — the plugin is current, or the release lookup failed — it * returns a payload with the installed version and no package, which core * files under `no_update`. That `no_update` entry is what sets core's * `update-supported` flag and makes the "Enable auto-updates" toggle * appear on the Plugins screen; without it, an off-directory plugin is * absent from the transient between releases and the toggle never shows. * * The incoming value is only passed through untouched for other plugins. */ public function provideUpdate( mixed $update, mixed $plugin_data, mixed $plugin_file ): mixed { if ( plugin_basename( USC_PLUGIN_FILE ) !== $plugin_file ) { return $update; } $release = $this->latestRelease(); if ( '' !== $release['version'] && '' !== $release['package'] && version_compare( $release['version'], USC_VERSION, '>' ) ) { return [ 'slug' => 'unsupervised-schedular', 'version' => $release['version'], 'url' => self::REPO_URL, 'package' => $release['package'], ]; } // No newer release: answer with a `no_update` payload so core keeps // the plugin in the update transient and shows the auto-update toggle. // The empty package leaves core nothing to auto-install, as intended. return [ 'slug' => 'unsupervised-schedular', 'version' => USC_VERSION, 'url' => self::REPO_URL, 'package' => '', ]; } /** * The latest published release, from the transient cache when fresh. * * @return array{version: string, package: string} Empty strings when no * usable release exists. */ private function latestRelease(): array { $cached = get_transient( self::TRANSIENT ); if ( is_array( $cached ) ) { return [ 'version' => Val::string( $cached['version'] ?? '' ), 'package' => Val::string( $cached['package'] ?? '' ), ]; } $release = $this->fetchLatestRelease(); set_transient( self::TRANSIENT, $release, self::CACHE_TTL ); return $release; } /** * Ask the Gitea API for the latest published release's version and zip asset. * * @return array{version: string, package: string} */ private function fetchLatestRelease(): array { $none = [ 'version' => '', 'package' => '', ]; $response = wp_remote_get( self::API_URL, [ 'timeout' => 10, 'headers' => [ 'Accept' => 'application/json' ], ] ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return $none; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) ) { return $none; } // Release tags are named v1.2.3; the plugin header carries the bare version. $version = preg_replace( '/^v/i', '', Val::string( $body['tag_name'] ?? '' ) ) ?? ''; // The release workflow attaches the built plugin zip (top-level // unsupervised-schedular/ folder, production autoloader) as an asset. // Gitea's auto-generated source archives are not usable packages. $package = ''; $assets = $body['assets'] ?? null; if ( is_array( $assets ) ) { foreach ( $assets as $asset ) { if ( ! is_array( $asset ) ) { continue; } if ( str_ends_with( strtolower( Val::string( $asset['name'] ?? '' ) ), '.zip' ) ) { $package = Val::string( $asset['browser_download_url'] ?? '' ); break; } } } if ( '' === $version || '' === $package ) { return $none; } return [ 'version' => $version, 'package' => $package, ]; } }