plugin_file, [$self, 'action_links']); } public function __construct() { $this->plugin_file = plugin_basename(BEEP_FILE); $this->plugin_slug = dirname($this->plugin_file); if ($this->plugin_slug === '.' || $this->plugin_slug === '') { $this->plugin_slug = 'beep'; } $this->version = BEEP_VERSION; $base = defined('BEEP_UPDATE_GITEA_BASE') ? BEEP_UPDATE_GITEA_BASE : 'https://git.sami'; $this->api_base = rtrim($base, '/'); $this->repo = defined('BEEP_UPDATE_REPO') ? BEEP_UPDATE_REPO : 'sami7777/beep'; } /** Resolve the access token (constant first, then filter). */ protected function token() { if (defined('BEEP_UPDATE_TOKEN') && BEEP_UPDATE_TOKEN) { return BEEP_UPDATE_TOKEN; } return (string) apply_filters('beep_update_token', ''); } protected function api_url($path) { return $this->api_base . '/api/v1/repos/' . $this->repo . $path; } /** * Fetch + cache the latest release. Returns an info array or null. */ protected function get_remote($force = false) { if (!$force) { $cached = get_site_transient(self::TRANSIENT); if ($cached !== false) { return is_array($cached) ? $cached : null; // '' means "checked, nothing" } } $headers = ['Accept' => 'application/json']; $token = $this->token(); if ($token) { $headers['Authorization'] = 'token ' . $token; } $res = wp_remote_get($this->api_url('/releases/latest'), [ 'timeout' => 15, 'headers' => $headers, ]); if (is_wp_error($res) || wp_remote_retrieve_response_code($res) !== 200) { // Brief negative cache so a flaky/unauthorized server doesn't stall every admin page. set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS); return null; } $data = json_decode(wp_remote_retrieve_body($res), true); if (!is_array($data) || empty($data['tag_name'])) { set_site_transient(self::TRANSIENT, '', HOUR_IN_SECONDS); return null; } $info = $this->parse_release($data); set_site_transient(self::TRANSIENT, $info, self::CACHE_HOURS * HOUR_IN_SECONDS); return $info; } /** Normalize a Gitea release payload into the fields we need. */ protected function parse_release($data) { $version = ltrim((string) $data['tag_name'], 'vV'); // Prefer an attached .zip asset; fall back to the source archive. $package = ''; if (!empty($data['assets']) && is_array($data['assets'])) { foreach ($data['assets'] as $asset) { if (!empty($asset['browser_download_url']) && strtolower(substr($asset['name'], -4)) === '.zip') { $package = $asset['browser_download_url']; break; } } } if (!$package) { $package = !empty($data['zipball_url']) ? $data['zipball_url'] : $this->api_base . '/' . $this->repo . '/archive/' . $data['tag_name'] . '.zip'; } return [ 'version' => $version, 'package' => $package, 'changelog' => isset($data['body']) ? (string) $data['body'] : '', 'published_at' => isset($data['published_at']) ? $data['published_at'] : '', 'name' => isset($data['name']) ? $data['name'] : '', 'html_url' => isset($data['html_url']) ? $data['html_url'] : ($this->api_base . '/' . $this->repo), ]; } /** Inject our plugin into the WordPress update transient. */ public function check_for_update($transient) { if (!is_object($transient)) { return $transient; } $info = $this->get_remote(); if (!$info) { return $transient; } $has_update = version_compare($info['version'], $this->version, '>'); $obj = (object) [ 'slug' => $this->plugin_slug, 'plugin' => $this->plugin_file, 'new_version' => $has_update ? $info['version'] : $this->version, 'url' => $info['html_url'], 'package' => $has_update ? $info['package'] : '', 'icons' => [], 'banners' => [], 'tested' => '', 'requires_php' => '', ]; if ($has_update) { $transient->response[$this->plugin_file] = $obj; } else { // Listing it as no_update keeps "View details" / auto-update UI working. if (!isset($transient->no_update)) { $transient->no_update = []; } $transient->no_update[$this->plugin_file] = $obj; } return $transient; } /** Provide the "View details" popup content. */ public function plugin_info($result, $action, $args) { if ($action !== 'plugin_information') { return $result; } if (empty($args->slug) || $args->slug !== $this->plugin_slug) { return $result; } $info = $this->get_remote(); if (!$info) { return $result; } $meta = get_file_data(BEEP_FILE, [ 'Name' => 'Plugin Name', 'Author' => 'Author', 'RequiresWP' => 'Requires at least', 'RequiresPHP' => 'Requires PHP', ]); return (object) [ 'name' => $meta['Name'] ?: 'Beep', 'slug' => $this->plugin_slug, 'version' => $info['version'], 'author' => esc_html($meta['Author']), 'homepage' => $info['html_url'], 'download_link' => $info['package'], 'requires' => $meta['RequiresWP'], 'requires_php' => $meta['RequiresPHP'], 'last_updated' => $info['published_at'], 'sections' => [ 'changelog' => $this->format_changelog($info['changelog']), ], ]; } /** Very small Markdown → HTML pass for the changelog body. */ protected function format_changelog($md) { if ($md === '') { return 'No changelog provided for this release.'; } $html = esc_html($md); $html = preg_replace('/^###\s+(.*)$/m', '