diff --git a/ansico-plugins-0.0.0.35.zip b/ansico-plugins-0.0.0.35.zip new file mode 100644 index 0000000..f99b4e2 Binary files /dev/null and b/ansico-plugins-0.0.0.35.zip differ 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 0000000..d6c1ec6 Binary files /dev/null and b/ansico-plugins-0.0.0.35.zip:Zone.Identifier differ diff --git a/ansico-plugins/README.md b/ansico-plugins/README.md new file mode 100644 index 0000000..6cd67c6 --- /dev/null +++ b/ansico-plugins/README.md @@ -0,0 +1,62 @@ +# Ansico Plugins + +Version: 0.0.0.1 +Forfatter: Andreas Andersen (Ansico) +URL: https://ansico.dk + +## Formål + +Dette plugin gør det muligt at forbinde et WordPress-site til en Forgejo-server og bruge udvalgte repositories som et privat plugin-katalog. + +## Hvad kan pluginet i denne version? + +- Gemme forbindelsesoplysninger til Forgejo +- Teste forbindelsen til Forgejo +- Hente repositories fra en bruger eller organisation +- Filtrere repositories på topic +- Vise seneste release for hvert repository +- Finde første ZIP-asset i seneste release +- Installere pluginet direkte fra den ZIP-fil + +## Sådan opsætter du det + +1. Installer og aktivér pluginet i WordPress. +2. Gå til **Indstillinger → Ansico Plugins**. +3. Udfyld: + - **Forgejo base URL** – fx `https://forgejo.example.com/` + - **Bruger eller organisation** – den konto der ejer repos + - **Owner-type** – `Bruger` eller `Organisation` + - **Access token** – nødvendigt for private repos + - **Topic-filter** – fx `wordpress-plugin` +4. Gem indstillinger. +5. Klik på **Test forbindelse**. +6. Gå til **Plugins → Ansico Plugins**. +7. Klik på **Installér / opdatér** ud for det ønskede plugin. + +## Krav til releases på Forgejo + +Hver plugin-release bør have en ZIP-asset som: +- ender på `.zip` +- indeholder plugin-mappen i roden +- indeholder en gyldig WordPress-pluginfil + +Eksempel: + +`mit-plugin.zip` +- `mit-plugin/` +- `mit-plugin/mit-plugin.php` +- `mit-plugin/includes/...` + +## Begrænsninger i version 0.0.0.1 + +- Pluginet bruger første ZIP-fil i seneste release +- Ingen automatisk versionssammenligning i WordPress’ normale update-UI endnu +- Ingen changelog-modal endnu +- Ingen cachelagring af API-kald endnu + +## Næste oplagte trin + +- Integrere med WordPress’ normale plugin-opdateringer +- Understøtte manifest-fil pr. repo +- Understøtte changelog og plugin-detaljer +- Tilføje caching og bedre fejlvisning diff --git a/ansico-plugins/ansico-plugins.php b/ansico-plugins/ansico-plugins.php new file mode 100644 index 0000000..536c75e --- /dev/null +++ b/ansico-plugins/ansico-plugins.php @@ -0,0 +1,147 @@ +admin = 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.