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', '

$1

', $html); $html = preg_replace('/^##\s+(.*)$/m', '

$1

', $html); $html = preg_replace('/^[\*\-]\s+(.*)$/m', '
  • $1
  • ', $html); $html = preg_replace('/(
  • .*<\/li>)/s', '', $html); return wpautop($html); } /** * Attach the auth token to any request aimed at our Gitea host so the * API check AND the package download both authenticate on a private repo. */ public function authorize_request($args, $url) { $token = $this->token(); if (!$token) { return $args; } $host = wp_parse_url($this->api_base, PHP_URL_HOST); $url_host = wp_parse_url($url, PHP_URL_HOST); if ($host && $url_host && strcasecmp($host, $url_host) === 0) { if (empty($args['headers']) || !is_array($args['headers'])) { $args['headers'] = []; } $args['headers']['Authorization'] = 'token ' . $token; } return $args; } /** * Gitea archives unzip to a folder like "beep" or "sami7777-beep-". * Rename it to the plugin slug so WP updates the existing folder in place. */ public function fix_source_dir($source, $remote_source, $upgrader, $hook_extra = null) { if (!is_array($hook_extra) || empty($hook_extra['plugin']) || $hook_extra['plugin'] !== $this->plugin_file) { return $source; } global $wp_filesystem; if (!$wp_filesystem) { return $source; } $desired = trailingslashit($remote_source) . $this->plugin_slug . '/'; if (untrailingslashit($source) === untrailingslashit($desired)) { return $source; } if ($wp_filesystem->move($source, $desired, true)) { return $desired; } return $source; } /** Drop the cache after an update so the new version is reflected at once. */ public function clear_cache($upgrader = null, $options = []) { delete_site_transient(self::TRANSIENT); } /** "Check for updates" link on the Plugins screen. */ public function action_links($links) { $url = wp_nonce_url( self_admin_url('plugins.php?beep_force_update_check=1'), 'beep_force_update_check' ); $links[] = 'Check for updates'; return $links; } } /** * Handle the manual "Check for updates" click: clear cache + force a re-check. */ add_action('admin_init', function () { if (empty($_GET['beep_force_update_check'])) { return; } if (!current_user_can('update_plugins') || !check_admin_referer('beep_force_update_check')) { return; } delete_site_transient(Beep_Updater::TRANSIENT); delete_site_transient('update_plugins'); wp_update_plugins(); wp_safe_redirect(self_admin_url('plugins.php')); exit; });