repo_url = $repo_url; $this->base_url = rtrim( $base_url, '/' ); $this->api_token = $api_token; } public function get_id(): string { return 'gitea'; } public function get_label(): string { return 'Gitea'; } public function get_repo_data(): ?array { $api_url = $this->base_url . '/api/v1/repos/' . $this->owner_repo_path(); if ( empty( $api_url ) ) { error_log( 'Projects Portfolio: Gitea repo URL is empty.' ); return null; } $cache_key = 'projects_portfolio_provider_data_' . md5( $api_url ); $cached_data = get_transient( $cache_key ); if ( $cached_data ) { return $cached_data; } $headers = [ 'Accept' => 'application/json' ]; if ( ! empty( $this->api_token ) ) { $headers['Authorization'] = 'token ' . $this->api_token; } else { error_log( 'Projects Portfolio: Gitea API token is missing. Using unauthenticated requests.' ); } $response = wp_remote_get( $api_url, [ 'headers' => $headers ] ); if ( is_wp_error( $response ) ) { error_log( 'Projects Portfolio: Gitea API error: ' . $response->get_error_message() ); return null; } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { error_log( 'Projects Portfolio: Gitea API error: Received status ' . $code ); return null; } $raw = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $raw ) || ! is_array( $raw ) ) { error_log( 'Projects Portfolio: Gitea API error: Invalid data received.' ); return null; } $owner = $raw['owner'] ?? []; $owner_login = $owner['login'] ?? $this->owner_login_from_path(); $data = [ 'owner' => [ 'avatar_url' => $owner['avatar_url'] ?? '', 'login' => $owner_login, 'html_url' => $owner['html_url'] ?? ( $this->base_url . '/' . $owner_login ), ], 'updated_at' => $raw['updated_at'] ?? '', 'language' => $raw['language'] ?? '', 'license' => $this->normalize_license( $raw['license'] ?? null ), 'stargazers_count' => (int) ( $raw['stars_count'] ?? 0 ), 'forks_count' => (int) ( $raw['forks_count'] ?? 0 ), 'open_issues_count' => (int) ( $raw['open_issues_count'] ?? 0 ), ]; set_transient( $cache_key, $data, HOUR_IN_SECONDS ); return $data; } public function get_release_url(): ?string { $release = $this->fetch_latest_release(); if ( null === $release ) { return null; } $assets = $release['assets'] ?? []; if ( is_array( $assets ) ) { foreach ( $assets as $asset ) { if ( isset( $asset['name'] ) && 'zip' === pathinfo( $asset['name'], PATHINFO_EXTENSION ) ) { $url = $asset['browser_download_url'] ?? null; if ( $url ) { return $url; } } } } $tag = $release['tag_name'] ?? ''; if ( '' === $tag ) { return null; } return $this->base_url . '/' . $this->owner_repo_path() . '/archive/refs/tags/' . $tag . '.zip'; } public function get_latest_version(): string { $release = $this->fetch_latest_release(); if ( null === $release ) { return 'Unknown'; } return isset( $release['tag_name'] ) ? (string) $release['tag_name'] : 'Unknown'; } public function get_repo_browse_url(): string { return $this->base_url . '/' . $this->owner_repo_path(); } public function get_owner_data( string $owner_login ): ?array { if ( empty( $owner_login ) ) { return null; } $api_url = $this->base_url . '/api/v1/users/' . rawurlencode( $owner_login ); $response = wp_remote_get( $api_url, [ 'headers' => $this->auth_headers() ] ); if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { return null; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) ) { return null; } return [ 'avatar_url' => $data['avatar_url'] ?? '', 'login' => $data['login'] ?? $owner_login, 'html_url' => $data['html_url'] ?? ( $this->base_url . '/' . $owner_login ), ]; } /** * Returns the first release object or null on failure / empty response. * Gitea's /releases/latest returns an array. */ private function fetch_latest_release(): ?array { $api_url = $this->base_url . '/api/v1/repos/' . $this->owner_repo_path() . '/releases/latest'; $cache_key = 'projects_portfolio_provider_release_' . md5( $api_url ); $cached = get_transient( $cache_key ); if ( $cached ) { return $cached; } $response = wp_remote_get( $api_url, [ 'headers' => $this->auth_headers() ] ); if ( is_wp_error( $response ) ) { error_log( 'Projects Portfolio: Gitea API error: ' . $response->get_error_message() ); return null; } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { error_log( 'Projects Portfolio: Gitea releases/latest returned status ' . $code ); return null; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) || empty( $body ) ) { return null; } $release = $body[0]; set_transient( $cache_key, $release, HOUR_IN_SECONDS ); return $release; } private function auth_headers(): array { $headers = [ 'Accept' => 'application/json' ]; if ( ! empty( $this->api_token ) ) { $headers['Authorization'] = 'token ' . $this->api_token; } return $headers; } private function owner_repo_path(): string { // Strip the base URL prefix if present, return "owner/repo". $path = $this->repo_url; if ( '' !== $this->base_url && 0 === strpos( $path, $this->base_url ) ) { $path = substr( $path, strlen( $this->base_url ) ); } $path = ltrim( $path, '/' ); // Drop trailing slashes and any "/archive/...", "/releases/...", etc. $path = preg_replace( '#/(archive|releases|tree|blob|issues|pulls|src|commits|wiki)(/.*)?$#', '', $path ); return trim( $path, '/' ); } private function owner_login_from_path(): string { $parts = explode( '/', $this->owner_repo_path() ); return $parts[0] ?? ''; } private function normalize_license( $license ): array { if ( is_array( $license ) ) { $name = $license['name'] ?? ''; return [ 'name' => '' !== $name ? $name : 'None' ]; } if ( is_string( $license ) && '' !== $license ) { return [ 'name' => $license ]; } return [ 'name' => 'None' ]; } }