settings = empty($settings) ? Ansico_Plugins::get_settings() : $settings; } private function base_url() { return trailingslashit((string) $this->settings['forgejo_base_url']); } private function owner_name() { return trim((string) $this->settings['forgejo_owner']); } private function owner_type() { $owner_type = isset($this->settings['owner_type']) ? strtolower((string) $this->settings['owner_type']) : 'user'; return in_array($owner_type, array('org', 'organisation', 'organization'), true) ? 'org' : 'user'; } private function cache_key($method, $url) { return 'ansico_plugins_' . md5(strtoupper((string) $method) . '|' . $url . '|' . wp_json_encode(array( 'token' => !empty($this->settings['access_token']) ? 1 : 0, 'ssl' => !empty($this->settings['verify_ssl']) ? 1 : 0, ))); } private function normalize_http_response($response) { return array( 'code' => (int) wp_remote_retrieve_response_code($response), 'message' => (string) wp_remote_retrieve_response_message($response), 'body' => (string) wp_remote_retrieve_body($response), 'headers' => array(), ); } private function parse_json_response($response, $error_code, $error_message) { if (is_wp_error($response)) { return $response; } $body = isset($response['body']) ? (string) $response['body'] : ''; $data = json_decode($body, true); if (JSON_ERROR_NONE !== json_last_error()) { return new WP_Error($error_code, $error_message . ' JSON error: ' . json_last_error_msg()); } return $data; } private function get_owner_endpoint() { $base = $this->base_url(); $owner = rawurlencode($this->owner_name()); if ('org' === $this->owner_type()) { return sprintf('%sapi/v1/orgs/%s/repos?limit=100', $base, $owner); } return sprintf('%sapi/v1/users/%s/repos?limit=100', $base, $owner); } public function test_connection() { $base = $this->base_url(); if ('' === $base) { return new WP_Error('missing_base_url', __('Forgejo base URL mangler.', 'ansico-plugins')); } $response = $this->request('GET', $base . 'api/v1/version', false); if (is_wp_error($response)) { return $response; } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('bad_response', sprintf(__('Forgejo svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $body = $this->parse_json_response($response, 'invalid_version', __('Kunne ikke læse version fra Forgejo.', 'ansico-plugins')); if (is_wp_error($body)) { return $body; } return array( 'version' => isset($body['version']) ? (string) $body['version'] : __('Ukendt', 'ansico-plugins'), ); } public function get_repositories_without_filter() { if ('' === $this->base_url() || '' === $this->owner_name()) { return new WP_Error('missing_settings', __('Du skal udfylde Forgejo URL og owner/org først.', 'ansico-plugins')); } $response = $this->request('GET', $this->get_owner_endpoint()); if (is_wp_error($response)) { return $response; } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('repos_http_error', sprintf(__('Repository-endpoint svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $repos = $this->parse_json_response($response, 'invalid_repos', __('Kunne ikke læse repositories fra Forgejo.', 'ansico-plugins')); if (is_wp_error($repos)) { return $repos; } return is_array($repos) ? $repos : array(); } public function get_repositories() { $all_repos = $this->get_repositories_without_filter(); if (is_wp_error($all_repos)) { return $all_repos; } $topic_filter = strtolower(trim((string) $this->settings['topic_filter'])); $filtered = array(); foreach ($all_repos as $repo) { if (empty($repo['name']) || empty($repo['owner']['login'])) { continue; } $topics = $this->get_repository_topics_safe($repo); $repo['topics'] = $topics; $normalized_topics = array_map('strtolower', array_map('strval', (array) $topics)); if ('' === $topic_filter || in_array($topic_filter, $normalized_topics, true)) { $filtered[] = $repo; } } return $filtered; } public function get_repositories_debug() { $all_repos = $this->get_repositories_without_filter(); if (is_wp_error($all_repos)) { return $all_repos; } $topic_filter = strtolower(trim((string) $this->settings['topic_filter'])); $rows = array(); foreach ($all_repos as $repo) { $owner = !empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''; $name = !empty($repo['name']) ? (string) $repo['name'] : ''; $topics_result = ($owner !== '' && $name !== '') ? $this->get_repository_topics($owner, $name) : new WP_Error('missing_repo_data', __('Repo mangler owner eller navn.', 'ansico-plugins')); $topics = is_wp_error($topics_result) ? array() : (array) $topics_result; $release = ($owner !== '' && $name !== '') ? $this->get_latest_release($owner, $name) : new WP_Error('missing_repo_data', __('Repo mangler owner eller navn.', 'ansico-plugins')); $rows[] = array( 'name' => $name, 'full_name' => !empty($repo['full_name']) ? (string) $repo['full_name'] : $name, 'topics' => $topics, 'matches_filter' => ('' === $topic_filter || in_array($topic_filter, array_map('strtolower', array_map('strval', $topics)), true)), 'release' => is_wp_error($release) ? '' : (!empty($release['tag_name']) ? (string) $release['tag_name'] : (!empty($release['name']) ? (string) $release['name'] : '')), 'topics_error' => is_wp_error($topics_result) ? $topics_result->get_error_message() : '', 'release_error' => is_wp_error($release) ? $release->get_error_message() : '', ); } return $rows; } private function get_repository_topics_safe($repo) { if (!empty($repo['topics']) && is_array($repo['topics'])) { return $repo['topics']; } $owner = !empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''; $name = !empty($repo['name']) ? (string) $repo['name'] : ''; if ('' === $owner || '' === $name) { return array(); } $topics = $this->get_repository_topics($owner, $name); if (!is_wp_error($topics)) { return (array) $topics; } $repo_details = $this->get_repository($owner, $name); if (!is_wp_error($repo_details) && !empty($repo_details['topics']) && is_array($repo_details['topics'])) { return $repo_details['topics']; } return array(); } public function get_repository($owner, $repo_name) { $endpoint = sprintf('%sapi/v1/repos/%s/%s', $this->base_url(), rawurlencode($owner), rawurlencode($repo_name)); $response = $this->request('GET', $endpoint); if (is_wp_error($response)) { return $response; } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('repo_http_error', sprintf(__('Repository-endpoint svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $repo = $this->parse_json_response($response, 'invalid_repo', __('Kunne ikke læse repository-data.', 'ansico-plugins')); return is_wp_error($repo) ? $repo : (is_array($repo) ? $repo : array()); } public function get_repository_topics($owner, $repo_name) { $endpoint = sprintf('%sapi/v1/repos/%s/%s/topics', $this->base_url(), rawurlencode($owner), rawurlencode($repo_name)); $response = $this->request('GET', $endpoint); if (is_wp_error($response)) { return $response; } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('topics_http_error', sprintf(__('Topics-endpoint svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $body = $this->parse_json_response($response, 'invalid_topics', __('Kunne ikke læse repository-topics.', 'ansico-plugins')); if (is_wp_error($body)) { return $body; } if (isset($body['topics']) && is_array($body['topics'])) { return $body['topics']; } return is_array($body) ? $body : array(); } public function get_latest_release($owner, $repo_name) { $endpoint = sprintf('%sapi/v1/repos/%s/%s/releases/latest', $this->base_url(), rawurlencode($owner), rawurlencode($repo_name)); $response = $this->request('GET', $endpoint); if (is_wp_error($response)) { return $response; } if (404 === (int) $response['code']) { return new WP_Error('release_not_found', __('Ingen release fundet.', 'ansico-plugins')); } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('release_http_error', sprintf(__('Release-endpoint svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $release = $this->parse_json_response($response, 'invalid_release', __('Kunne ikke læse release-data.', 'ansico-plugins')); return is_wp_error($release) ? $release : (is_array($release) ? $release : array()); } public function find_zip_asset($release) { if (empty($release['assets']) || !is_array($release['assets'])) { return null; } foreach ($release['assets'] as $asset) { $name = isset($asset['name']) ? (string) $asset['name'] : ''; if (preg_match('/\.zip$/i', $name)) { return $asset; } } if (!empty($release['zipball_url'])) { return array( 'name' => basename((string) $release['zipball_url']), 'browser_download_url' => (string) $release['zipball_url'], ); } return null; } public function get_repository_contents($owner, $repo_name, $path = '', $ref = '') { $endpoint = sprintf('%sapi/v1/repos/%s/%s/contents', $this->base_url(), rawurlencode($owner), rawurlencode($repo_name)); $path = ltrim((string) $path, '/'); if ('' !== $path) { $endpoint .= '/' . str_replace('%2F', '/', rawurlencode($path)); } $query = array(); if ('' !== (string) $ref) { $query['ref'] = (string) $ref; } if (!empty($query)) { $endpoint .= '?' . http_build_query($query, '', '&'); } $response = $this->request('GET', $endpoint); if (is_wp_error($response)) { return $response; } if (404 === (int) $response['code']) { return new WP_Error('contents_not_found', __('Fil eller mappe blev ikke fundet i repository.', 'ansico-plugins')); } if ($response['code'] < 200 || $response['code'] >= 300) { return new WP_Error('contents_http_error', sprintf(__('Contents-endpoint svarede med HTTP %d.', 'ansico-plugins'), (int) $response['code'])); } $contents = $this->parse_json_response($response, 'invalid_contents', __('Kunne ikke læse repository-indhold.', 'ansico-plugins')); return is_wp_error($contents) ? $contents : $contents; } public function get_repository_readme_title($owner, $repo_name, $ref = '') { $documentation = $this->get_repository_documentation($owner, $repo_name, $ref); if (is_wp_error($documentation)) { return ''; } return !empty($documentation['title']) ? (string) $documentation['title'] : ''; } public function get_repository_documentation($owner, $repo_name, $ref = '') { $candidates = array('readme.txt', 'README.txt', 'README.md', 'readme.md', 'Readme.md'); foreach ($candidates as $candidate) { $raw = $this->get_repository_text_file($owner, $repo_name, $candidate, $ref); if ('' === $raw) { continue; } if (preg_match('/\.txt$/i', $candidate)) { $parsed = $this->parse_wordpress_readme($raw); } else { $parsed = $this->parse_markdown_readme($raw); } if (!empty($parsed['title']) || !empty($parsed['short_description']) || !empty($parsed['sections'])) { $parsed['source'] = $candidate; return $parsed; } } return array( 'title' => '', 'short_description' => '', 'sections' => array(), 'source' => '', ); } private function get_repository_text_file($owner, $repo_name, $path, $ref = '') { $file = $this->get_repository_contents($owner, $repo_name, $path, $ref); if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) { return ''; } $raw = base64_decode((string) $file['content'], true); if (false === $raw) { return ''; } return str_replace(array(" ", " "), " ", (string) $raw); } private function parse_wordpress_readme($raw) { $raw = trim((string) $raw); $lines = preg_split('/ /', $raw); $title = ''; $short_description = ''; $sections = array(); $current = ''; $buffer = array(); $header_done = false; foreach ($lines as $line) { if ('' === $title && preg_match('/^===\s*(.+?)\s*===\s*$/', $line, $m)) { $title = trim($m[1]); continue; } if (!$header_done && preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) { $header_done = true; $current = strtolower(trim($m[1])); $buffer = array(); continue; } if (!$header_done) { if ('' === $short_description && '' !== trim($line) && false === strpos($line, ':')) { $short_description = trim($line); } continue; } if (preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) { if ('' !== $current) { $sections[$current] = trim(implode(" ", $buffer)); } $current = strtolower(trim($m[1])); $buffer = array(); continue; } $buffer[] = $line; } if ('' !== $current) { $sections[$current] = trim(implode(" ", $buffer)); } if ('' === $short_description && !empty($sections['description'])) { $short_description = $this->first_paragraph($sections['description']); } return array( 'title' => $title, 'short_description' => $short_description, 'sections' => $sections, ); } private function parse_markdown_readme($raw) { $title = ''; $short_description = ''; $sections = array(); $current = ''; $buffer = array(); $after_title = ''; foreach (preg_split('/ /', (string) $raw) as $line) { if ('' === $title && preg_match('/^#\s+(.+)$/', $line, $m)) { $title = trim(wp_strip_all_tags($m[1])); continue; } if (preg_match('/^##\s+(.+)$/', $line, $m)) { if ('' !== $current) { $sections[$current] = trim(implode(" ", $buffer)); } $current = strtolower(trim(wp_strip_all_tags($m[1]))); $buffer = array(); continue; } if ('' === $current) { $after_title .= $line . " "; } else { $buffer[] = $line; } } if ('' !== $current) { $sections[$current] = trim(implode(" ", $buffer)); } $short_description = $this->first_paragraph($after_title); if ('' === $short_description && !empty($sections['description'])) { $short_description = $this->first_paragraph($sections['description']); } return array( 'title' => $title, 'short_description' => $short_description, 'sections' => $sections, ); } private function first_paragraph($text) { $text = trim((string) $text); if ('' === $text) { return ''; } $parts = preg_split("/ \s* /", $text); if (empty($parts[0])) { return ''; } return trim(wp_strip_all_tags($parts[0])); } private function find_repository_asset_url($owner, $repo_name, $default_branch, $candidates) { foreach ((array) $candidates as $candidate) { $file = $this->get_repository_contents($owner, $repo_name, $candidate, $default_branch); if (!is_wp_error($file) && is_array($file) && !empty($file['download_url'])) { return (string) $file['download_url']; } } return ''; } public function get_repository_plugin_metadata($owner, $repo_name) { $repo = $this->get_repository($owner, $repo_name); if (is_wp_error($repo)) { return $repo; } $default_branch = !empty($repo['default_branch']) ? (string) $repo['default_branch'] : ''; $root = $this->get_repository_contents($owner, $repo_name, '', $default_branch); $plugin_headers = array(); if (!is_wp_error($root) && is_array($root)) { foreach ($root as $item) { if (!is_array($item) || empty($item['name'])) { continue; } $name = (string) $item['name']; if (empty($plugin_headers) && !empty($item['type']) && 'file' === $item['type'] && preg_match('/\.php$/i', $name)) { $file = $this->get_repository_contents($owner, $repo_name, $name, $default_branch); if (!is_wp_error($file) && is_array($file) && !empty($file['content']) && !empty($file['encoding']) && 'base64' === $file['encoding']) { $raw = base64_decode((string) $file['content'], true); if (false !== $raw) { $plugin_headers = $this->parse_plugin_headers($raw); } } } } } $documentation = $this->get_repository_documentation($owner, $repo_name, $default_branch); if (is_wp_error($documentation)) { $documentation = array('title' => '', 'short_description' => '', 'sections' => array(), 'source' => ''); } $author_name = !empty($plugin_headers['Author']) ? (string) $plugin_headers['Author'] : (!empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : '')); $author_url = !empty($plugin_headers['AuthorURI']) ? (string) $plugin_headers['AuthorURI'] : (!empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : '')); $support_url = !empty($plugin_headers['SupportURI']) ? (string) $plugin_headers['SupportURI'] : (!empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : ''); $license = !empty($plugin_headers['License']) ? (string) $plugin_headers['License'] : (!empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : ''); $screenshot_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('screenshot.png', 'assets/screenshot.png')); $icon_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('icon-256x256.png', 'icon-128x128.png', 'assets/icon-256x256.png', 'assets/icon-128x128.png')); $banner_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('banner-1544x500.png', 'banner-772x250.png', 'assets/banner-1544x500.png', 'assets/banner-772x250.png')); return array( 'readme_title' => !empty($documentation['title']) ? $documentation['title'] : '', 'description' => !empty($documentation['short_description']) ? $documentation['short_description'] : (!empty($repo['description']) ? (string) $repo['description'] : ''), 'readme_sections' => !empty($documentation['sections']) && is_array($documentation['sections']) ? $documentation['sections'] : array(), 'readme_source' => !empty($documentation['source']) ? $documentation['source'] : '', 'author_name' => $author_name, 'author_url' => $author_url, 'plugin_headers' => $plugin_headers, 'support_url' => $support_url, 'license' => $license, 'screenshot_url' => $screenshot_url, 'icon_url' => $icon_url, 'banner_url' => $banner_url, 'default_branch' => $default_branch, ); } private function parse_plugin_headers($content) { $headers = array( 'Plugin Name' => 'Name', 'Author' => 'Author', 'Author URI' => 'AuthorURI', 'Plugin URI' => 'PluginURI', 'Description' => 'Description', 'Version' => 'Version', 'Requires at least' => 'RequiresWP', 'Requires PHP' => 'RequiresPHP', 'License' => 'License', 'License URI' => 'LicenseURI', 'Text Domain' => 'TextDomain', 'Support URI' => 'SupportURI', ); $parsed = array(); foreach ($headers as $label => $key) { if (preg_match('/^[ \t\/*#@]*' . preg_quote($label, '/') . ':(.*)$/mi', (string) $content, $matches)) { $parsed[$key] = trim(wp_strip_all_tags($matches[1])); } } return $parsed; } public function request($method, $url, $allow_retry_without_auth = true) { $method = strtoupper((string) $method); $headers = array( 'Accept' => 'application/json', 'User-Agent' => 'Ansico-Plugins/' . ANSICO_PLUGINS_VERSION . '; ' . home_url('/'), ); if (!empty($this->settings['access_token'])) { $headers['Authorization'] = 'token ' . trim((string) $this->settings['access_token']); } $args = array( 'method' => $method, 'headers' => $headers, 'timeout' => max(5, (int) $this->settings['request_timeout']), 'sslverify' => !empty($this->settings['verify_ssl']), ); $cache_key = $this->cache_key($method, $url); if ('GET' === $method) { $cached = get_transient($cache_key); if (is_array($cached) && isset($cached['body'], $cached['code'])) { return $cached; } } $response = wp_remote_request(esc_url_raw($url), $args); if (is_wp_error($response)) { return $response; } $normalized = $this->normalize_http_response($response); if ( $allow_retry_without_auth && !empty($this->settings['access_token']) && in_array((int) $normalized['code'], array(401, 403), true) ) { unset($args['headers']['Authorization']); $retry_response = wp_remote_request(esc_url_raw($url), $args); if (!is_wp_error($retry_response)) { $normalized = $this->normalize_http_response($retry_response); } } if ('GET' === $method) { set_transient($cache_key, $normalized, 5 * MINUTE_IN_SECONDS); } return $normalized; } }