From 759abdc332ac2910e29c321523a6b4769a8b8e90 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 18 Apr 2026 22:50:31 +0200 Subject: [PATCH] Sync folders --- ansico-plugins-0.0.0.35.zip | Bin 0 -> 21374 bytes ansico-plugins-0.0.0.35.zip:Zone.Identifier | Bin 0 -> 25 bytes ansico-plugins/README.md | 62 ++ ansico-plugins/ansico-plugins.php | 147 ++++ .../includes/class-ansico-plugins-admin.php | 658 ++++++++++++++++++ .../includes/class-ansico-plugins-client.php | 463 ++++++++++++ .../class-ansico-plugins-installer.php | 192 +++++ .../includes/class-ansico-plugins-updater.php | 307 ++++++++ ansico-plugins/readme.txt | 56 ++ 9 files changed, 1885 insertions(+) create mode 100644 ansico-plugins-0.0.0.35.zip create mode 100644 ansico-plugins-0.0.0.35.zip:Zone.Identifier create mode 100644 ansico-plugins/README.md create mode 100644 ansico-plugins/ansico-plugins.php create mode 100644 ansico-plugins/includes/class-ansico-plugins-admin.php create mode 100644 ansico-plugins/includes/class-ansico-plugins-client.php create mode 100644 ansico-plugins/includes/class-ansico-plugins-installer.php create mode 100644 ansico-plugins/includes/class-ansico-plugins-updater.php create mode 100644 ansico-plugins/readme.txt diff --git a/ansico-plugins-0.0.0.35.zip b/ansico-plugins-0.0.0.35.zip new file mode 100644 index 0000000000000000000000000000000000000000..f99b4e24d090638bd5640bbf4ce720512d9b27d8 GIT binary patch literal 21374 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 literal 0 HcmV?d00001 diff --git a/ansico-plugins-0.0.0.35.zip:Zone.Identifier b/ansico-plugins-0.0.0.35.zip:Zone.Identifier new file mode 100644 index 0000000000000000000000000000000000000000..d6c1ec682968c796b9f5e9e080cc6f674b57c766 GIT binary patch literal 25 dcma!!%Fjy;DN4*MPD?F{<>dl#JyUFr831@K2xadmin = new Ansico_Plugins_Admin(); + $this->installer = new Ansico_Plugins_Installer(); + $this->updater = new Ansico_Plugins_Updater(); + + add_action('admin_init', array($this, 'maybe_handle_actions')); + add_filter('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'plugin_action_links')); + } + + public static function default_settings() { + return array( + 'forgejo_base_url' => 'https://ansico.dk', + 'forgejo_owner' => 'Ansico', + 'owner_type' => 'org', + 'access_token' => '', + 'topic_filter' => 'wordpress-plugin', + 'verify_ssl' => 1, + 'request_timeout' => 20, + 'self_update_enabled' => 1, + 'self_update_owner' => 'Ansico', + 'self_update_repo' => 'Ansico-plugins', + ); + + } + + public static function get_settings() { + $settings = get_option(ANSICO_PLUGINS_OPTION, array()); + return wp_parse_args($settings, self::default_settings()); + } + + + public static function clear_http_cache() { + global $wpdb; + $like = $wpdb->esc_like('_transient_ansico_plugins_') . '%'; + $like_timeout = $wpdb->esc_like('_transient_timeout_ansico_plugins_') . '%'; + $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $like, $like_timeout)); + } + + public static function get_managed_plugins() { + $managed = get_option(ANSICO_PLUGINS_MANAGED_OPTION, array()); + return is_array($managed) ? $managed : array(); + } + + public static function set_managed_plugin($plugin_file, $meta) { + $managed = self::get_managed_plugins(); + $managed[$plugin_file] = wp_parse_args($meta, array( + 'slug' => dirname($plugin_file), + 'owner' => '', + 'repo' => '', + 'description' => '', + 'repo_html_url' => '', + )); + update_option(ANSICO_PLUGINS_MANAGED_OPTION, $managed, false); + } + + public function plugin_action_links($links) { + $settings_link = sprintf( + '%s', + esc_url(admin_url('options-general.php?page=ansico-plugins')), + esc_html__('Indstillinger', 'ansico-plugins') + ); + array_unshift($links, $settings_link); + return $links; + } + + public function maybe_handle_actions() { + if (!is_admin() || !current_user_can('install_plugins')) { + return; + } + + if (empty($_GET['ansico_action'])) { + return; + } + + $action = sanitize_key(wp_unslash($_GET['ansico_action'])); + + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'ansico_plugins_action')) { + wp_die(esc_html__('Ugyldig sikkerheds-token.', 'ansico-plugins')); + } + + if ('install_latest' === $action) { + $repo = isset($_GET['repo']) ? sanitize_text_field(wp_unslash($_GET['repo'])) : ''; + $slug = isset($_GET['slug']) ? sanitize_title(wp_unslash($_GET['slug'])) : ''; + $this->installer->install_latest_release($repo, $slug); + } + + if ('delete_plugin' === $action) { + $repo = isset($_GET['repo']) ? sanitize_text_field(wp_unslash($_GET['repo'])) : ''; + $plugin_file = isset($_GET['plugin_file']) ? sanitize_text_field(wp_unslash($_GET['plugin_file'])) : ''; + $this->installer->delete_plugin($repo, $plugin_file); + } + } +} + +register_activation_hook(__FILE__, function() { + if (!get_option(ANSICO_PLUGINS_OPTION)) { + add_option(ANSICO_PLUGINS_OPTION, Ansico_Plugins::default_settings()); + } + if (!get_option(ANSICO_PLUGINS_MANAGED_OPTION)) { + add_option(ANSICO_PLUGINS_MANAGED_OPTION, array()); + } +}); + +Ansico_Plugins::instance(); diff --git a/ansico-plugins/includes/class-ansico-plugins-admin.php b/ansico-plugins/includes/class-ansico-plugins-admin.php new file mode 100644 index 0000000..37b1faa --- /dev/null +++ b/ansico-plugins/includes/class-ansico-plugins-admin.php @@ -0,0 +1,658 @@ + __('Forgejo base URL', 'ansico-plugins'), + 'forgejo_owner' => __('Bruger eller organisation', 'ansico-plugins'), + 'access_token' => __('Access token', 'ansico-plugins'), + 'topic_filter' => __('Topic-filter', 'ansico-plugins'), + 'request_timeout' => __('Timeout (sekunder)', 'ansico-plugins'), + ); + + foreach ($fields as $key => $label) { + add_settings_field($key, $label, array($this, 'render_field'), 'ansico-plugins', 'ansico_plugins_connection', array('key' => $key)); + } + + add_settings_field('owner_type', __('Owner-type', 'ansico-plugins'), array($this, 'render_owner_type_field'), 'ansico-plugins', 'ansico_plugins_connection'); + add_settings_field('verify_ssl', __('SSL-verifikation', 'ansico-plugins'), array($this, 'render_verify_ssl_field'), 'ansico-plugins', 'ansico_plugins_connection'); + + add_settings_section( + 'ansico_plugins_self_update', + __('Opdatering af Ansico Plugins', 'ansico-plugins'), + '__return_false', + 'ansico-plugins' + ); + + add_settings_field('self_update_enabled', __('Slå selvopdatering til', 'ansico-plugins'), array($this, 'render_self_update_enabled_field'), 'ansico-plugins', 'ansico_plugins_self_update'); + add_settings_field('self_update_owner', __('Ansico plugins owner', 'ansico-plugins'), array($this, 'render_field'), 'ansico-plugins', 'ansico_plugins_self_update', array('key' => 'self_update_owner')); + add_settings_field('self_update_repo', __('Ansico plugins repository', 'ansico-plugins'), array($this, 'render_field'), 'ansico-plugins', 'ansico_plugins_self_update', array('key' => 'self_update_repo')); + } + + public function sanitize_settings($input) { + $output = Ansico_Plugins::get_settings(); + $output['forgejo_base_url'] = isset($input['forgejo_base_url']) ? esc_url_raw(trim((string) $input['forgejo_base_url'])) : ''; + $output['forgejo_owner'] = isset($input['forgejo_owner']) ? sanitize_text_field($input['forgejo_owner']) : ''; + $output['owner_type'] = isset($input['owner_type']) && in_array($input['owner_type'], array('user', 'org'), true) ? $input['owner_type'] : 'org'; + $output['access_token'] = isset($input['access_token']) ? sanitize_text_field($input['access_token']) : ''; + $output['topic_filter'] = isset($input['topic_filter']) ? sanitize_text_field($input['topic_filter']) : 'wordpress-plugin'; + $output['verify_ssl'] = !empty($input['verify_ssl']) ? 1 : 0; + $output['request_timeout'] = isset($input['request_timeout']) ? max(5, (int) $input['request_timeout']) : 20; + $output['self_update_enabled'] = !empty($input['self_update_enabled']) ? 1 : 0; + $output['self_update_owner'] = isset($input['self_update_owner']) ? sanitize_text_field($input['self_update_owner']) : ''; + $output['self_update_repo'] = isset($input['self_update_repo']) ? sanitize_text_field($input['self_update_repo']) : ''; + Ansico_Plugins::clear_http_cache(); + wp_clean_plugins_cache(true); + delete_site_transient('update_plugins'); + return $output; + } + + + public function maybe_handle_reset_defaults() { + if (!is_admin() || !current_user_can('manage_options') || empty($_GET['ansico_reset_defaults'])) { + return; + } + + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'ansico_plugins_reset_defaults')) { + return; + } + + update_option(ANSICO_PLUGINS_OPTION, Ansico_Plugins::default_settings(), false); + Ansico_Plugins::clear_http_cache(); + wp_clean_plugins_cache(true); + delete_site_transient('update_plugins'); + + wp_safe_redirect(admin_url('options-general.php?page=ansico-plugins&ansico_reset_done=1')); + exit; + } + + public function maybe_show_reset_notice() { + if (!current_user_can('manage_options') || empty($_GET['ansico_reset_done'])) { + return; + } + + echo '

' . esc_html__('Ansico standardindstillinger er gendannet.', 'ansico-plugins') . '

'; + } + + public function render_field($args) { + $settings = Ansico_Plugins::get_settings(); + $key = $args['key']; + $value = isset($settings[$key]) ? $settings[$key] : ''; + $type = 'text'; + + if ('access_token' === $key) { + $type = 'password'; + } elseif ('request_timeout' === $key) { + $type = 'number'; + } elseif ('forgejo_base_url' === $key) { + $type = 'url'; + } + + printf( + '', + esc_attr($type), + esc_attr(ANSICO_PLUGINS_OPTION), + esc_attr($key), + esc_attr($value), + 'request_timeout' === $key ? 'min="5" step="1"' : '' + ); + + $help = array( + 'forgejo_base_url' => __('Eksempel: https://forgejo.ditdomæne.dk/', 'ansico-plugins'), + 'forgejo_owner' => __('Navn på bruger eller organisation som ejer repositories.', 'ansico-plugins'), + 'access_token' => __('PAT med adgang til at læse repos/releases. Kræves ofte for private repos.', 'ansico-plugins'), + 'topic_filter' => __('Kun repos med dette topic vises. Brug tomt felt for at vise alle repos.', 'ansico-plugins'), + 'request_timeout' => __('Øg værdien hvis Forgejo-serveren er langsom.', 'ansico-plugins'), + 'self_update_owner' => __('Organisation eller bruger som ejer Ansico Plugins-repository.', 'ansico-plugins'), + 'self_update_repo' => __('Repository-navn for Ansico Plugins, fx Ansico-plugins.', 'ansico-plugins'), + ); + + if (isset($help[$key])) { + echo '

' . esc_html($help[$key]) . '

'; + } + } + + public function render_owner_type_field() { + $settings = Ansico_Plugins::get_settings(); + ?> +
+ +

+ + +

+ + +

+ +
+

+

+ +
+ ' . esc_html__('Reset til Ansico standard', 'ansico-plugins') . ''; + ?> +
+ +

+ + +

+ +
+

+
    +
  1. +
  2. +
  3. +
  4. +
  5. +
  6. +
  7. +
+
+ test_connection(); + + if (is_wp_error($result)) { + printf( + '

%s %s

', + esc_html__('Forgejo-forbindelse fejlede:', 'ansico-plugins'), + esc_html($result->get_error_message()) + ); + return; + } + + printf( + '

%s %s

', + esc_html__('Forbindelse OK. Forgejo-version:', 'ansico-plugins'), + esc_html($result['version']) + ); + } + + public function render_catalog_page() { + if (!current_user_can('install_plugins')) { + return; + } + + $client = new Ansico_Plugins_Client(); + $repos = $client->get_repositories(); + + if (isset($_GET['ansico_view']) && 'details' === sanitize_key(wp_unslash($_GET['ansico_view']))) { + $this->render_details_page($client, $repos, false); + return; + } + + $settings = Ansico_Plugins::get_settings(); + $owner_label = !empty($settings['forgejo_owner']) ? $settings['forgejo_owner'] : __('din organisation', 'ansico-plugins'); + $forgejo_link = !empty($settings['forgejo_base_url']) ? trailingslashit($settings['forgejo_base_url']) . rawurlencode($owner_label) : ''; + ?> +
+ render_catalog_styles(); ?> +

+ +
+ +
+
+

%2$s.', 'ansico-plugins'), + array('a' => array('href' => array(), 'target' => array(), 'rel' => array())) + ), + esc_url($forgejo_link), + esc_html($owner_label) + ); ?>

+ get_self_plugin_status_markup($client); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> +
+
+ +

%s

', esc_html($repos->get_error_message())); + echo '

' . esc_html__('Gå til indstillinger', 'ansico-plugins') . '

'; + echo ''; + return; + } + + if (empty($repos)) { + echo '

' . esc_html__('Ingen repositories matchede de valgte kriterier.', 'ansico-plugins') . '

'; + echo '

' . esc_html__('Forgejo svarer, men ingen repos matchede topic-filteret. Kontrollér topic-navn og gem indstillingerne igen for at rydde cache.', 'ansico-plugins') . '

'; + echo ''; + return; + } + + if (!function_exists('get_plugins')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $installed_plugins = get_plugins(); + $managed_plugins = Ansico_Plugins::get_managed_plugins(); + + echo '
'; + foreach ($repos as $repo) { + $release = $client->get_latest_release($repo['owner']['login'], $repo['name']); + $zip_asset = !is_wp_error($release) ? $client->find_zip_asset($release) : null; + $plugin_state = $this->get_plugin_state($repo, $installed_plugins, $managed_plugins, $release); + $metadata = $client->get_repository_plugin_metadata($repo['owner']['login'], $repo['name']); + $metadata = is_wp_error($metadata) ? array() : $metadata; + $display_name = !empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name']; + $details_url = $this->get_details_url($repo['name']); + $badge = $this->get_state_badge($plugin_state); + $version_label = is_wp_error($release) ? '—' : $this->normalize_release_version($release); + + echo '
'; + echo '
'; + echo '
AP
'; + echo '
'; + echo '

' . esc_html($display_name) . '

'; + echo '
' . $badge . '
'; + echo '
'; + + echo '

' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '

'; + + echo '
'; + echo '' . esc_html__('Installeret', 'ansico-plugins') . ': ' . esc_html($plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—') . ''; + echo '' . esc_html__('Seneste', 'ansico-plugins') . ': ' . esc_html($version_label) . ''; + echo '
'; + + echo '
'; + foreach ($this->build_action_buttons($repo, $plugin_state, $zip_asset) as $button_html) { + echo $button_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + echo '' . esc_html__('Vis detaljer', 'ansico-plugins') . ''; + echo '
'; + echo $this->get_details_inline_markup($repo, $release, $zip_asset, $plugin_state, $metadata); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + } + echo '
'; + } + + private function render_details_page($client, $repos, $is_modal = false) { + $repo_name = isset($_GET['repo']) ? sanitize_text_field(wp_unslash($_GET['repo'])) : ''; + $repo = null; + + if (!is_wp_error($repos)) { + foreach ($repos as $candidate) { + if (!empty($candidate['name']) && $candidate['name'] === $repo_name) { + $repo = $candidate; + break; + } + } + } + + if (!$repo) { + wp_die(esc_html__('Repository blev ikke fundet.', 'ansico-plugins')); + } + + if (!function_exists('get_plugins')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $installed_plugins = get_plugins(); + $managed_plugins = Ansico_Plugins::get_managed_plugins(); + $release = $client->get_latest_release($repo['owner']['login'], $repo['name']); + $zip_asset = !is_wp_error($release) ? $client->find_zip_asset($release) : null; + $plugin_state = $this->get_plugin_state($repo, $installed_plugins, $managed_plugins, $release); + $metadata = $client->get_repository_plugin_metadata($repo['owner']['login'], $repo['name']); + $metadata = is_wp_error($metadata) ? array() : $metadata; + $back_url = admin_url('plugins.php?page=ansico-plugins-catalog'); + + echo '
'; + $this->render_catalog_styles(); + echo '

' . esc_html(!empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name']) . '

'; + echo '' . esc_html__('Tilbage til katalog', 'ansico-plugins') . ''; + echo '
'; + echo $this->get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo '
'; + exit; + } + + private function build_action_buttons($repo, $plugin_state, $zip_asset) { + $buttons = array(); + $repo_name = $repo['name']; + $slug = $repo['name']; + + if (!$plugin_state['is_installed'] && $zip_asset) { + $buttons[] = '' . esc_html__('Installér', 'ansico-plugins') . ''; + return $buttons; + } + + if ($plugin_state['is_installed'] && !$plugin_state['is_active']) { + $buttons[] = '' . esc_html__('Aktivér', 'ansico-plugins') . ''; + $buttons[] = '' . esc_html__('Afinstallér', 'ansico-plugins') . ''; + return $buttons; + } + + if ($plugin_state['is_installed'] && $plugin_state['has_update']) { + $buttons[] = '' . esc_html__('Opdatér', 'ansico-plugins') . ''; + $buttons[] = '' . esc_html__('Afinstallér', 'ansico-plugins') . ''; + return $buttons; + } + + if ($plugin_state['is_installed'] && $plugin_state['is_active']) { + $buttons[] = '' . esc_html__('Geninstallér', 'ansico-plugins') . ''; + $buttons[] = '' . esc_html__('Afinstallér', 'ansico-plugins') . ''; + } + + return $buttons; + } + + private function get_plugin_state($repo, $installed_plugins, $managed_plugins, $release) { + $plugin_file = ''; + foreach ($managed_plugins as $candidate_plugin_file => $meta) { + if (!empty($meta['owner']) && !empty($meta['repo']) && $meta['owner'] === $repo['owner']['login'] && $meta['repo'] === $repo['name']) { + $plugin_file = $candidate_plugin_file; + break; + } + } + + if ('' === $plugin_file) { + foreach ($installed_plugins as $candidate_plugin_file => $plugin_data) { + if (dirname($candidate_plugin_file) === sanitize_title($repo['name'])) { + $plugin_file = $candidate_plugin_file; + break; + } + } + } + + $is_installed = '' !== $plugin_file && isset($installed_plugins[$plugin_file]); + $installed_version = $is_installed && !empty($installed_plugins[$plugin_file]['Version']) ? (string) $installed_plugins[$plugin_file]['Version'] : ''; + $is_active = $is_installed ? is_plugin_active($plugin_file) : false; + $remote_version = !is_wp_error($release) ? $this->normalize_release_version($release) : ''; + $has_update = $is_installed && '' !== $installed_version && '' !== $remote_version && version_compare($remote_version, $installed_version, '>'); + + return array( + 'plugin_file' => $plugin_file, + 'is_installed' => $is_installed, + 'is_active' => $is_active, + 'installed_version' => $installed_version, + 'remote_version' => $remote_version, + 'has_update' => $has_update, + ); + } + + private function normalize_release_version($release) { + $version = ''; + if (!empty($release['tag_name'])) { + $version = (string) $release['tag_name']; + } elseif (!empty($release['name'])) { + $version = (string) $release['name']; + } + $version = ltrim($version, 'vV'); + return '' !== $version ? $version : '—'; + } + + private function get_state_badge($plugin_state) { + if (!$plugin_state['is_installed']) { + if (empty($plugin_state['remote_version']) || '—' === $plugin_state['remote_version']) { + return '' . esc_html__('Ikke udgivet', 'ansico-plugins') . ''; + } + return '' . esc_html__('Ikke installeret', 'ansico-plugins') . ''; + } + if ($plugin_state['has_update']) { + return '' . esc_html__('Opdatering klar', 'ansico-plugins') . ''; + } + if (!$plugin_state['is_active']) { + return '' . esc_html__('Installeret', 'ansico-plugins') . ''; + } + return '' . esc_html__('Aktiv', 'ansico-plugins') . ''; + } + + private function get_self_plugin_status_markup($client) { + $settings = Ansico_Plugins::get_settings(); + if (empty($settings['self_update_enabled']) || empty($settings['self_update_owner']) || empty($settings['self_update_repo'])) { + return ''; + } + + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $plugin_file = plugin_basename(ANSICO_PLUGINS_FILE); + $plugin_data = get_plugin_data(ANSICO_PLUGINS_FILE, false, false); + $current_version = !empty($plugin_data['Version']) ? (string) $plugin_data['Version'] : ANSICO_PLUGINS_VERSION; + $release = $client->get_latest_release($settings['self_update_owner'], $settings['self_update_repo']); + + if (is_wp_error($release)) { + return '

' . esc_html__('Ansico Plugins: kunne ikke hente opdateringsstatus.', 'ansico-plugins') . '

'; + } + + $remote_version = $this->normalize_release_version($release); + $repo_url = trailingslashit($settings['forgejo_base_url']) . rawurlencode($settings['self_update_owner']) . '/' . rawurlencode($settings['self_update_repo']); + + if ('' === $remote_version || '—' === $remote_version) { + return '

' . esc_html__('Ansico Plugins: ingen udgivet version fundet endnu.', 'ansico-plugins') . '

'; + } + + if (version_compare($remote_version, $current_version, '>')) { + $update_url = esc_url(admin_url('plugins.php')); + return '

' . sprintf( + wp_kses( + __('Ansico Plugins: version %1$s er installeret, og version %2$s er tilgængelig fra %4$s. Opdater fra den almindelige Plugins-side.', 'ansico-plugins'), + array('a' => array('href' => array(), 'target' => array(), 'rel' => array())) + ), + esc_html($current_version), + esc_html($remote_version), + esc_url($repo_url), + esc_html($settings['self_update_repo']), + esc_url($update_url) + ) . '

'; + } + + return '

' . sprintf( + wp_kses( + __('Ansico Plugins er opdateret. Installeret version %1$s matcher seneste release i %3$s.', 'ansico-plugins'), + array('a' => array('href' => array(), 'target' => array(), 'rel' => array())) + ), + esc_html($current_version), + esc_url($repo_url), + esc_html($settings['self_update_repo']) + ) . '

'; + } + + private function get_details_url($repo_name) { + return '#TB_inline?width=920&height=648&inlineId=ansico-plugin-details-' . rawurlencode(sanitize_title($repo_name)); + } + + private function build_action_url($action, $repo_name, $slug, $plugin_file = '') { + $url = admin_url('plugins.php?page=ansico-plugins-catalog&ansico_action=' . rawurlencode($action) . '&repo=' . rawurlencode($repo_name) . '&slug=' . rawurlencode($slug)); + if ('' !== $plugin_file) { + $url .= '&plugin_file=' . rawurlencode($plugin_file); + } + return wp_nonce_url($url, 'ansico_plugins_action'); + } + + private function get_details_inline_markup($repo, $release, $zip_asset, $plugin_state, $metadata) { + $id = 'ansico-plugin-details-' . sanitize_title($repo['name']); + return ''; + } + + private function get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata) { + $display_name = !empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name']; + $html = ''; + $html .= '
'; + $html .= '
AP
'; + $html .= '
'; + $html .= '

' . esc_html($display_name) . '

'; + $html .= '

' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '

'; + $html .= $this->get_state_badge($plugin_state); + $html .= '
'; + + $html .= '
'; + foreach ($this->build_action_buttons($repo, $plugin_state, $zip_asset) as $button_html) { + $html .= $button_html; + } + $html .= '' . esc_html__('Åbn repository', 'ansico-plugins') . ''; + $html .= '
'; + + $html .= '
'; + $meta_rows = array( + __('Installeret version', 'ansico-plugins') => $plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—', + __('Seneste release', 'ansico-plugins') => !is_wp_error($release) ? $this->normalize_release_version($release) : '—', + __('Repository', 'ansico-plugins') => !empty($repo['full_name']) ? $repo['full_name'] : $repo['name'], + __('ZIP-fil', 'ansico-plugins') => $zip_asset && !empty($zip_asset['name']) ? $zip_asset['name'] : '—', + __('Forfatter', 'ansico-plugins') => !empty($metadata['author_name']) ? $metadata['author_name'] : '—', + __('Forfatter-URL', 'ansico-plugins') => !empty($metadata['author_url']) ? $metadata['author_url'] : '—', + __('Support-URL', 'ansico-plugins') => !empty($metadata['support_url']) ? $metadata['support_url'] : '—', + __('Licens', 'ansico-plugins') => !empty($metadata['license']) ? $metadata['license'] : '—', + __('Plugin-URL', 'ansico-plugins') => !empty($metadata['plugin_headers']['PluginURI']) ? $metadata['plugin_headers']['PluginURI'] : '—', + __('Kræver WordPress', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresWP']) ? $metadata['plugin_headers']['RequiresWP'] : '—', + __('Kræver PHP', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresPHP']) ? $metadata['plugin_headers']['RequiresPHP'] : '—', + __('Text domain', 'ansico-plugins') => !empty($metadata['plugin_headers']['TextDomain']) ? $metadata['plugin_headers']['TextDomain'] : '—', + ); + foreach ($meta_rows as $label => $value) { + $html .= '

' . esc_html($label) . '
'; + if (is_string($value) && preg_match('#^https?://#i', $value)) { + $html .= '' . esc_html($value) . ''; + } else { + $html .= esc_html($value); + } + $html .= '

'; + } + $html .= '
'; + + if (!empty($metadata['screenshot_url'])) { + $html .= '
'; + $html .= '

' . esc_html__('Screenshot', 'ansico-plugins') . '

'; + $html .= '' . esc_attr(sprintf(__('Screenshot for %s', 'ansico-plugins'), $display_name)) . ''; + $html .= '
'; + } + + if (!is_wp_error($release) && !empty($release['body'])) { + $html .= '

' . esc_html__('Release-noter', 'ansico-plugins') . '

'; + $html .= '
' . wp_kses_post(wpautop($release['body'])) . '
'; + } + + return $html; + } + + private function render_catalog_styles() { + echo ''; + } +} diff --git a/ansico-plugins/includes/class-ansico-plugins-client.php b/ansico-plugins/includes/class-ansico-plugins-client.php new file mode 100644 index 0000000..9f6d06d --- /dev/null +++ b/ansico-plugins/includes/class-ansico-plugins-client.php @@ -0,0 +1,463 @@ +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 = '') { + $candidates = array('README.md', 'readme.md', 'Readme.md'); + foreach ($candidates as $candidate) { + $file = $this->get_repository_contents($owner, $repo_name, $candidate, $ref); + if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) { + continue; + } + + $raw = base64_decode((string) $file['content'], true); + if (false === $raw) { + continue; + } + + if (preg_match('/^\#\s+(.+)$/m', (string) $raw, $matches)) { + return trim(wp_strip_all_tags($matches[1])); + } + } + + 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); + if (is_wp_error($root) || !is_array($root)) { + return array( + 'readme_title' => '', + 'author_name' => !empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''), + 'author_url' => !empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : ''), + 'plugin_headers' => array(), + 'support_url' => !empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : '', + 'license' => !empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : '', + 'screenshot_url' => '', + 'default_branch' => $default_branch, + ); + } + + $screenshot_url = ''; + $plugin_headers = array(); + foreach ($root as $item) { + if (!is_array($item) || empty($item['name'])) { + continue; + } + $name = (string) $item['name']; + if (strtolower($name) === 'screenshot.png' && !empty($item['download_url'])) { + $screenshot_url = (string) $item['download_url']; + } + 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); + } + } + } + } + + $readme_title = $this->get_repository_readme_title($owner, $repo_name, $default_branch); + $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'] : ''); + + return array( + 'readme_title' => $readme_title, + 'author_name' => $author_name, + 'author_url' => $author_url, + 'plugin_headers' => $plugin_headers, + 'support_url' => $support_url, + 'license' => $license, + 'screenshot_url' => $screenshot_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; + } +} diff --git a/ansico-plugins/includes/class-ansico-plugins-installer.php b/ansico-plugins/includes/class-ansico-plugins-installer.php new file mode 100644 index 0000000..9c81322 --- /dev/null +++ b/ansico-plugins/includes/class-ansico-plugins-installer.php @@ -0,0 +1,192 @@ +get_latest_release($owner, $repo_name); + if (is_wp_error($release)) { + wp_die(esc_html($release->get_error_message())); + } + + $zip_asset = $client->find_zip_asset($release); + if (!$zip_asset || empty($zip_asset['browser_download_url'])) { + wp_die(esc_html__('Ingen installérbar ZIP-fil fundet i seneste release.', 'ansico-plugins')); + } + + add_filter('http_request_args', array($this, 'inject_auth_header'), 10, 2); + + if (!function_exists('get_current_screen')) { + require_once ABSPATH . 'wp-admin/includes/screen.php'; + } + + set_current_screen('plugins_page_ansico-plugins-catalog'); + require_once ABSPATH . 'wp-admin/admin-header.php'; + + $existing_plugin_file = $this->find_existing_plugin_file($owner, $repo_name, $slug); + $is_update = '' !== $existing_plugin_file; + + echo '
'; + echo '

' . esc_html(sprintf($is_update ? __('Opdaterer %s', 'ansico-plugins') : __('Installerer %s', 'ansico-plugins'), $repo_name)) . '

'; + + $skin = new Automatic_Upgrader_Skin(); + $upgrader = new Plugin_Upgrader($skin); + + if ($is_update) { + $result = $upgrader->run(array( + 'package' => $zip_asset['browser_download_url'], + 'destination' => WP_PLUGIN_DIR, + 'clear_destination' => true, + 'clear_working' => true, + 'hook_extra' => array( + 'plugin' => $existing_plugin_file, + 'type' => 'plugin', + 'action' => 'update', + ), + )); + } else { + $result = $upgrader->install($zip_asset['browser_download_url']); + } + + remove_filter('http_request_args', array($this, 'inject_auth_header'), 10); + + if (is_wp_error($result) || false === $result) { + $message = is_wp_error($result) ? $result->get_error_message() : __('Plugin installation mislykkedes.', 'ansico-plugins'); + echo '

' . esc_html($message) . '

'; + echo '

' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '

'; + echo '
'; + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + + $plugin_file = $is_update ? $existing_plugin_file : $upgrader->plugin_info(); + if ($plugin_file) { + $repo = $client->get_repository($owner, $repo_name); + Ansico_Plugins::set_managed_plugin($plugin_file, array( + 'slug' => $slug, + 'owner' => $owner, + 'repo' => $repo_name, + 'description' => !is_wp_error($repo) && !empty($repo['description']) ? $repo['description'] : '', + 'repo_html_url' => !is_wp_error($repo) && !empty($repo['html_url']) ? $repo['html_url'] : '', + )); + wp_clean_plugins_cache(true); + } + + echo '

' . esc_html($is_update ? __('Plugin opdateret.', 'ansico-plugins') : __('Plugin installeret.', 'ansico-plugins')) . '

'; + echo '

' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . ' '; + + if ($plugin_file && function_exists('is_plugin_inactive') && is_plugin_inactive($plugin_file)) { + $activate_url = wp_nonce_url(admin_url('plugins.php?action=activate&plugin=' . rawurlencode($plugin_file)), 'activate-plugin_' . $plugin_file); + echo '' . esc_html__('Aktivér plugin', 'ansico-plugins') . ' '; + } + + echo '' . esc_html__('Gå til Plugins', 'ansico-plugins') . '

'; + echo ''; + + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + + public function delete_plugin($repo_name, $plugin_file) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + + if ('' === $plugin_file || !file_exists(WP_PLUGIN_DIR . '/' . ltrim($plugin_file, '/'))) { + wp_die(esc_html__('Pluginfil blev ikke fundet.', 'ansico-plugins')); + } + + if (!function_exists('deactivate_plugins')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + if (!function_exists('get_current_screen')) { + require_once ABSPATH . 'wp-admin/includes/screen.php'; + } + + set_current_screen('plugins_page_ansico-plugins-catalog'); + require_once ABSPATH . 'wp-admin/admin-header.php'; + + echo '
'; + echo '

' . esc_html(sprintf(__('Afinstallerer %s', 'ansico-plugins'), $repo_name)) . '

'; + + if (is_plugin_active($plugin_file)) { + deactivate_plugins($plugin_file, true); + } + + $result = delete_plugins(array($plugin_file)); + if (is_wp_error($result) || !$result) { + $message = is_wp_error($result) ? $result->get_error_message() : __('Plugin kunne ikke afinstalleres.', 'ansico-plugins'); + echo '

' . esc_html($message) . '

'; + echo '

' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '

'; + echo '
'; + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + + $managed = Ansico_Plugins::get_managed_plugins(); + if (isset($managed[$plugin_file])) { + unset($managed[$plugin_file]); + update_option(ANSICO_PLUGINS_MANAGED_OPTION, $managed, false); + } + wp_clean_plugins_cache(true); + + echo '

' . esc_html__('Plugin afinstalleret.', 'ansico-plugins') . '

'; + echo '

' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '

'; + echo ''; + + require_once ABSPATH . 'wp-admin/admin-footer.php'; + exit; + } + + private function find_existing_plugin_file($owner, $repo_name, $slug) { + if (!function_exists('get_plugins')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $managed_plugins = Ansico_Plugins::get_managed_plugins(); + foreach ($managed_plugins as $plugin_file => $meta) { + if (!empty($meta['owner']) && !empty($meta['repo']) && $meta['owner'] === $owner && $meta['repo'] === $repo_name) { + return $plugin_file; + } + } + + $plugins = get_plugins(); + $slug = sanitize_title($slug); + foreach ($plugins as $plugin_file => $data) { + if (dirname($plugin_file) === $slug) { + return $plugin_file; + } + } + + return ''; + } + + public function inject_auth_header($args, $url) { + $settings = Ansico_Plugins::get_settings(); + $base = trailingslashit($settings['forgejo_base_url']); + + if (empty($settings['access_token']) || strpos($url, $base) !== 0) { + return $args; + } + + if (empty($args['headers']) || !is_array($args['headers'])) { + $args['headers'] = array(); + } + + $args['headers']['Authorization'] = 'token ' . trim((string) $settings['access_token']); + $args['sslverify'] = !empty($settings['verify_ssl']); + return $args; + } +} diff --git a/ansico-plugins/includes/class-ansico-plugins-updater.php b/ansico-plugins/includes/class-ansico-plugins-updater.php new file mode 100644 index 0000000..a98e463 --- /dev/null +++ b/ansico-plugins/includes/class-ansico-plugins-updater.php @@ -0,0 +1,307 @@ +checked) || !is_array($transient->checked)) { + return $transient; + } + + $managed_plugins = Ansico_Plugins::get_managed_plugins(); + if (empty($managed_plugins)) { + return $transient; + } + + if (!isset($transient->response) || !is_array($transient->response)) { + $transient->response = array(); + } + + if (!isset($transient->no_update) || !is_array($transient->no_update)) { + $transient->no_update = array(); + } + + $client = new Ansico_Plugins_Client(); + + foreach ($managed_plugins as $plugin_file => $meta) { + if (empty($transient->checked[$plugin_file])) { + continue; + } + + $owner = !empty($meta['owner']) ? $meta['owner'] : ''; + $repo = !empty($meta['repo']) ? $meta['repo'] : ''; + if ('' === $owner || '' === $repo) { + continue; + } + + $release = $client->get_latest_release($owner, $repo); + if (is_wp_error($release)) { + continue; + } + + $zip_asset = $client->find_zip_asset($release); + if (!$zip_asset || empty($zip_asset['browser_download_url'])) { + continue; + } + + $remote_version = $this->normalize_version($release); + $current_version = (string) $transient->checked[$plugin_file]; + $plugin_data = $this->get_plugin_header($plugin_file); + $slug = !empty($meta['slug']) ? $meta['slug'] : dirname($plugin_file); + + $item = (object) array( + 'id' => $plugin_file, + 'slug' => $slug, + 'plugin' => $plugin_file, + 'new_version' => $remote_version, + 'url' => !empty($meta['repo_html_url']) ? $meta['repo_html_url'] : '', + 'package' => $zip_asset['browser_download_url'], + 'icons' => array(), + 'banners' => array(), + 'banners_rtl' => array(), + 'tested' => !empty($plugin_data['RequiresWP']) ? $plugin_data['RequiresWP'] : '', + 'requires_php' => !empty($plugin_data['RequiresPHP']) ? $plugin_data['RequiresPHP'] : '', + 'compatibility' => new stdClass(), + ); + + if (version_compare($remote_version, $current_version, '>')) { + $transient->response[$plugin_file] = $item; + unset($transient->no_update[$plugin_file]); + } else { + $transient->no_update[$plugin_file] = $item; + unset($transient->response[$plugin_file]); + } + } + + $transient = $this->inject_self_update_data($transient, $client); + + return $transient; + } + + public function plugins_api($result, $action, $args) { + if ('plugin_information' !== $action || empty($args->slug)) { + return $result; + } + + $client = new Ansico_Plugins_Client(); + + if ($this->is_self_plugin_slug($args->slug)) { + $self_info = $this->get_self_update_info($client); + if (!$self_info) { + return $result; + } + + $sections = array( + 'description' => !empty($self_info['repo_meta']['plugin_headers']['Description']) ? wp_kses_post(wpautop($self_info['repo_meta']['plugin_headers']['Description'])) : wp_kses_post(wpautop(__('Ansico Plugins forbinder WordPress med private Forgejo-hostede plugins.', 'ansico-plugins'))), + 'installation' => wp_kses_post(wpautop(__('Installeres manuelt første gang. Derefter kan pluginet opdatere sig selv via Forgejo releases.', 'ansico-plugins'))), + 'changelog' => !empty($self_info['release']['body']) ? wp_kses_post(wpautop($self_info['release']['body'])) : wp_kses_post(wpautop(__('Ingen changelog tilgængelig.', 'ansico-plugins'))), + ); + + return (object) array( + 'name' => !empty($self_info['repo_meta']['readme_title']) ? $self_info['repo_meta']['readme_title'] : 'Ansico Plugins', + 'slug' => dirname(plugin_basename(ANSICO_PLUGINS_FILE)), + 'version' => $this->normalize_version($self_info['release']), + 'author' => !empty($self_info['repo_meta']['author_name']) ? '' . esc_html($self_info['repo_meta']['author_name']) . '' : 'Andreas Andersen (Ansico)', + 'author_profile' => !empty($self_info['repo_meta']['author_url']) ? $self_info['repo_meta']['author_url'] : 'https://ansico.dk', + 'homepage' => !empty($self_info['repo']['html_url']) ? $self_info['repo']['html_url'] : '', + 'requires' => !empty($self_info['plugin_data']['RequiresWP']) ? $self_info['plugin_data']['RequiresWP'] : '', + 'requires_php' => !empty($self_info['plugin_data']['RequiresPHP']) ? $self_info['plugin_data']['RequiresPHP'] : '', + 'tested' => get_bloginfo('version'), + 'last_updated' => !empty($self_info['release']['published_at']) ? gmdate('Y-m-d', strtotime($self_info['release']['published_at'])) : '', + 'download_link' => (!empty($self_info['zip_asset']['browser_download_url'])) ? $self_info['zip_asset']['browser_download_url'] : '', + 'sections' => $sections, + 'banners' => array(), + 'icons' => !empty($self_info['repo_meta']['screenshot_url']) ? array('default' => $self_info['repo_meta']['screenshot_url']) : array(), + 'external' => true, + ); + } + + $managed_plugins = Ansico_Plugins::get_managed_plugins(); + if (empty($managed_plugins)) { + return $result; + } + + $matched = null; + foreach ($managed_plugins as $plugin_file => $meta) { + if (!empty($meta['slug']) && $meta['slug'] === $args->slug) { + $matched = array('plugin_file' => $plugin_file, 'meta' => $meta); + break; + } + } + + if (!$matched) { + return $result; + } + + $release = $client->get_latest_release($matched['meta']['owner'], $matched['meta']['repo']); + if (is_wp_error($release)) { + return $result; + } + + $zip_asset = $client->find_zip_asset($release); + $plugin_data = $this->get_plugin_header($matched['plugin_file']); + $repo_meta = $client->get_repository_plugin_metadata($matched['meta']['owner'], $matched['meta']['repo']); + $repo_meta = is_wp_error($repo_meta) ? array() : $repo_meta; + $sections = array( + 'description' => !empty($matched['meta']['description']) ? wp_kses_post(wpautop($matched['meta']['description'])) : wp_kses_post(wpautop(__('Privat plugin distribueret via Forgejo.', 'ansico-plugins'))), + 'installation' => wp_kses_post(wpautop(__('Installeres og opdateres via Ansico Plugins og Forgejo releases.', 'ansico-plugins'))), + 'changelog' => !empty($release['body']) ? wp_kses_post(wpautop($release['body'])) : wp_kses_post(wpautop(__('Ingen changelog tilgængelig.', 'ansico-plugins'))), + ); + + return (object) array( + 'name' => !empty($plugin_data['Name']) ? $plugin_data['Name'] : $matched['meta']['repo'], + 'slug' => $matched['meta']['slug'], + 'version' => $this->normalize_version($release), + 'author' => !empty($repo_meta['author_name']) ? '' . esc_html($repo_meta['author_name']) . '' : 'Andreas Andersen (Ansico)', + 'author_profile' => !empty($repo_meta['author_url']) ? $repo_meta['author_url'] : 'https://ansico.dk', + 'homepage' => !empty($matched['meta']['repo_html_url']) ? $matched['meta']['repo_html_url'] : '', + 'requires' => !empty($plugin_data['RequiresWP']) ? $plugin_data['RequiresWP'] : '', + 'requires_php' => !empty($plugin_data['RequiresPHP']) ? $plugin_data['RequiresPHP'] : '', + 'tested' => get_bloginfo('version'), + 'last_updated' => !empty($release['published_at']) ? gmdate('Y-m-d', strtotime($release['published_at'])) : '', + 'download_link' => $zip_asset ? $zip_asset['browser_download_url'] : '', + 'sections' => $sections, + 'banners' => array(), + 'icons' => !empty($repo_meta['screenshot_url']) ? array('default' => $repo_meta['screenshot_url']) : array(), + 'external' => true, + ); + } + + + private function inject_self_update_data($transient, $client) { + $settings = Ansico_Plugins::get_settings(); + if (empty($settings['self_update_enabled']) || empty($settings['self_update_owner']) || empty($settings['self_update_repo'])) { + return $transient; + } + + $plugin_file = plugin_basename(ANSICO_PLUGINS_FILE); + if (empty($transient->checked[$plugin_file])) { + return $transient; + } + + $release = $client->get_latest_release($settings['self_update_owner'], $settings['self_update_repo']); + if (is_wp_error($release)) { + return $transient; + } + + $zip_asset = $client->find_zip_asset($release); + if (!$zip_asset || empty($zip_asset['browser_download_url'])) { + return $transient; + } + + $remote_version = $this->normalize_version($release); + $current_version = (string) $transient->checked[$plugin_file]; + $repo = $client->get_repository($settings['self_update_owner'], $settings['self_update_repo']); + $repo_url = (!is_wp_error($repo) && !empty($repo['html_url'])) ? (string) $repo['html_url'] : ''; + + $item = (object) array( + 'id' => $plugin_file, + 'slug' => dirname($plugin_file), + 'plugin' => $plugin_file, + 'new_version' => $remote_version, + 'url' => $repo_url, + 'package' => $zip_asset['browser_download_url'], + 'icons' => array(), + 'banners' => array(), + 'banners_rtl' => array(), + 'tested' => '', + 'requires_php' => '', + 'compatibility' => new stdClass(), + ); + + if (version_compare($remote_version, $current_version, '>')) { + $transient->response[$plugin_file] = $item; + unset($transient->no_update[$plugin_file]); + } else { + $transient->no_update[$plugin_file] = $item; + unset($transient->response[$plugin_file]); + } + + return $transient; + } + + private function is_self_plugin_slug($slug) { + return in_array((string) $slug, array('ansico-plugins', dirname(plugin_basename(ANSICO_PLUGINS_FILE))), true); + } + + private function get_self_update_info($client) { + $settings = Ansico_Plugins::get_settings(); + if (empty($settings['self_update_enabled']) || empty($settings['self_update_owner']) || empty($settings['self_update_repo'])) { + return null; + } + + $release = $client->get_latest_release($settings['self_update_owner'], $settings['self_update_repo']); + if (is_wp_error($release)) { + return null; + } + + $repo = $client->get_repository($settings['self_update_owner'], $settings['self_update_repo']); + $repo_meta = $client->get_repository_plugin_metadata($settings['self_update_owner'], $settings['self_update_repo']); + $zip_asset = $client->find_zip_asset($release); + $plugin_data = $this->get_plugin_header(plugin_basename(ANSICO_PLUGINS_FILE)); + + return array( + 'settings' => $settings, + 'release' => $release, + 'repo' => is_wp_error($repo) ? array() : $repo, + 'repo_meta' => is_wp_error($repo_meta) ? array() : $repo_meta, + 'zip_asset' => $zip_asset, + 'plugin_data' => $plugin_data, + ); + } + + public function inject_auth_header($args, $url) { + $settings = Ansico_Plugins::get_settings(); + $base = trailingslashit($settings['forgejo_base_url']); + + if (empty($settings['access_token']) || empty($base) || strpos($url, $base) !== 0) { + return $args; + } + + if (empty($args['headers']) || !is_array($args['headers'])) { + $args['headers'] = array(); + } + + $args['headers']['Authorization'] = 'token ' . trim((string) $settings['access_token']); + $args['sslverify'] = !empty($settings['verify_ssl']); + return $args; + } + + private function normalize_version($release) { + $version = ''; + + if (!empty($release['tag_name'])) { + $version = (string) $release['tag_name']; + } elseif (!empty($release['name'])) { + $version = (string) $release['name']; + } + + $version = ltrim($version, 'vV'); + return '' !== $version ? $version : '0.0.0'; + } + + private function get_plugin_header($plugin_file) { + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $path = WP_PLUGIN_DIR . '/' . ltrim($plugin_file, '/'); + if (!file_exists($path)) { + return array(); + } + + return get_plugin_data($path, false, false); + } +} diff --git a/ansico-plugins/readme.txt b/ansico-plugins/readme.txt new file mode 100644 index 0000000..a6acc28 --- /dev/null +++ b/ansico-plugins/readme.txt @@ -0,0 +1,56 @@ +=== Ansico Plugins === +Contributors: ansico +Tags: forgejo, plugins, private directory +Requires at least: 6.3 +Tested up to: 6.8 +Requires PHP: 7.4 +Stable tag: 0.0.0.3 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Privat plugin-directory til Forgejo. + +== Description == + +Ansico Plugins forbinder et WordPress-site til en Forgejo-server og viser udvalgte repositories som et privat plugin-katalog. + +Funktioner i version 0.0.0.2: +- Indstillingsside under Indstillinger → Ansico Plugins +- Test af Forgejo-forbindelse +- Filtrering af repositories via topic +- Oversigt over repos under Plugins → Ansico Plugins +- Installation/opdatering fra første ZIP-fil i seneste release +- Native update-check i WordPress for plugins installeret via Ansico Plugins +- Enkel plugin-information og changelog til WordPress' detaljevisning +- Enkel caching af Forgejo API-kald + +== Installation == + +1. Upload plugin-mappen til /wp-content/plugins/ eller installer ZIP-filen. +2. Aktivér pluginet i WordPress. +3. Gå til Indstillinger → Ansico Plugins. +4. Udfyld Forgejo base URL, owner/org og eventuelt access token. +5. Gem indstillinger og test forbindelsen. +6. Gå til Plugins → Ansico Plugins og installér fra seneste release. +7. Senere vil WordPress kunne opdage nye releases som opdateringer for plugins, der er installeret via Ansico Plugins. + +== Release workflow på Forgejo == + +1. Marker relevante repos med topic "wordpress-plugin" eller et andet topic efter eget valg. +2. Opret en release. +3. Upload en ZIP-fil som release-asset. +4. Sørg for at ZIP-filen er installérbar i WordPress og har plugin-mappen i roden. + +== Changelog == + += 0.0.0.2 = +* Tilføjet native opdateringskontrol i WordPress for plugins installeret via Ansico Plugins. +* Tilføjet plugin-information/changelog via plugins_api. +* Tilføjet enkel caching af Forgejo API-kald. + += 0.0.0.1 = +* Første MVP-version. + += 0.0.0.3 = +* Fixed organization repository topic filtering by fetching repo details when needed. +* Topic filter is now case-insensitive.