Sync folders

This commit is contained in:
Andreas Andersen 2026-04-18 22:50:31 +02:00
parent 9a3cd23150
commit 759abdc332
9 changed files with 1885 additions and 0 deletions

BIN
ansico-plugins-0.0.0.35.zip Normal file

Binary file not shown.

Binary file not shown.

62
ansico-plugins/README.md Normal file
View file

@ -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

View file

@ -0,0 +1,147 @@
<?php
/**
* Plugin Name: Ansico Plugins
* Plugin URI: https://ansico.dk
* Description: Privat plugin-directory til Forgejo. Giver overblik over plugins, test af forbindelse og mulighed for installation/opdatering fra seneste release-ZIP.
* Version: 0.0.0.35
* Author: Andreas Andersen (Ansico)
* Author URI: https://ansico.dk
* Text Domain: ansico-plugins
* Requires at least: 6.3
* Requires PHP: 7.4
*/
if (!defined('ABSPATH')) {
exit;
}
define('ANSICO_PLUGINS_VERSION', '0.0.0.35');
define('ANSICO_PLUGINS_MANAGED_OPTION', 'ansico_plugins_managed_plugins');
define('ANSICO_PLUGINS_FILE', __FILE__);
define('ANSICO_PLUGINS_DIR', plugin_dir_path(__FILE__));
define('ANSICO_PLUGINS_URL', plugin_dir_url(__FILE__));
define('ANSICO_PLUGINS_OPTION', 'ansico_plugins_settings');
require_once ANSICO_PLUGINS_DIR . 'includes/class-ansico-plugins-client.php';
require_once ANSICO_PLUGINS_DIR . 'includes/class-ansico-plugins-admin.php';
require_once ANSICO_PLUGINS_DIR . 'includes/class-ansico-plugins-installer.php';
require_once ANSICO_PLUGINS_DIR . 'includes/class-ansico-plugins-updater.php';
final class Ansico_Plugins {
private static $instance = null;
private $admin;
private $installer;
private $updater;
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->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(
'<a href="%s">%s</a>',
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();

View file

@ -0,0 +1,658 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class Ansico_Plugins_Admin {
public function __construct() {
add_action('admin_menu', array($this, 'register_admin_pages'));
add_action('admin_init', array($this, 'register_settings'));
add_action('admin_init', array($this, 'maybe_handle_reset_defaults'));
add_action('admin_notices', array($this, 'maybe_show_test_notice'));
add_action('admin_notices', array($this, 'maybe_show_reset_notice'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
}
public function enqueue_admin_assets($hook_suffix) {
if ('plugins_page_ansico-plugins-catalog' !== $hook_suffix) {
return;
}
add_thickbox();
}
public function register_admin_pages() {
add_options_page(
__('Ansico Plugins', 'ansico-plugins'),
__('Ansico Plugins', 'ansico-plugins'),
'manage_options',
'ansico-plugins',
array($this, 'render_settings_page')
);
add_plugins_page(
__('Ansico Plugins', 'ansico-plugins'),
__('Ansico Plugins', 'ansico-plugins'),
'install_plugins',
'ansico-plugins-catalog',
array($this, 'render_catalog_page')
);
}
public function register_settings() {
register_setting('ansico_plugins_group', ANSICO_PLUGINS_OPTION, array($this, 'sanitize_settings'));
add_settings_section(
'ansico_plugins_connection',
__('Forbindelse til Forgejo', 'ansico-plugins'),
'__return_false',
'ansico-plugins'
);
$fields = array(
'forgejo_base_url' => __('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 '<div class="notice notice-success is-dismissible"><p>' . esc_html__('Ansico standardindstillinger er gendannet.', 'ansico-plugins') . '</p></div>';
}
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(
'<input type="%1$s" class="regular-text" name="%2$s[%3$s]" value="%4$s" %5$s />',
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 '<p class="description">' . esc_html($help[$key]) . '</p>';
}
}
public function render_owner_type_field() {
$settings = Ansico_Plugins::get_settings();
?>
<label><input type="radio" name="<?php echo esc_attr(ANSICO_PLUGINS_OPTION); ?>[owner_type]" value="user" <?php checked($settings['owner_type'], 'user'); ?> /> <?php esc_html_e('Bruger', 'ansico-plugins'); ?></label><br />
<label><input type="radio" name="<?php echo esc_attr(ANSICO_PLUGINS_OPTION); ?>[owner_type]" value="org" <?php checked($settings['owner_type'], 'org'); ?> /> <?php esc_html_e('Organisation', 'ansico-plugins'); ?></label>
<p class="description"><?php esc_html_e('Vælg om owner-feltet henviser til en bruger eller en organisation i Forgejo.', 'ansico-plugins'); ?></p>
<?php
}
public function render_verify_ssl_field() {
$settings = Ansico_Plugins::get_settings();
?>
<label>
<input type="checkbox" name="<?php echo esc_attr(ANSICO_PLUGINS_OPTION); ?>[verify_ssl]" value="1" <?php checked(!empty($settings['verify_ssl'])); ?> />
<?php esc_html_e('Verificér SSL-certifikat', 'ansico-plugins'); ?>
</label>
<p class="description"><?php esc_html_e('Bør normalt være slået til. Slå kun fra ved selvsignerede certifikater i testmiljøer.', 'ansico-plugins'); ?></p>
<?php
}
public function render_self_update_enabled_field() {
$settings = Ansico_Plugins::get_settings();
?>
<label>
<input type="checkbox" name="<?php echo esc_attr(ANSICO_PLUGINS_OPTION); ?>[self_update_enabled]" value="1" <?php checked(!empty($settings['self_update_enabled'])); ?> />
<?php esc_html_e('Lad Ansico Plugins søge efter opdateringer til sig selv via et separat repository.', 'ansico-plugins'); ?>
</label>
<p class="description"><?php esc_html_e('Bruger samme Forgejo base URL, token og SSL-indstillinger som ovenfor.', 'ansico-plugins'); ?></p>
<?php
}
public function render_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
$test_url = wp_nonce_url(admin_url('options-general.php?page=ansico-plugins&ansico_test_connection=1'), 'ansico_plugins_test_connection');
?>
<div class="wrap">
<h1><?php esc_html_e('Ansico Plugins', 'ansico-plugins'); ?></h1>
<p><?php esc_html_e("Forbind WordPress Ansico's Forgejo-server og få adgang til Ansico plugins.", 'ansico-plugins'); ?></p>
<form method="post" action="options.php">
<?php
settings_fields('ansico_plugins_group');
do_settings_sections('ansico-plugins');
submit_button(__('Gem indstillinger', 'ansico-plugins'));
$reset_url = wp_nonce_url(admin_url('options-general.php?page=ansico-plugins&ansico_reset_defaults=1'), 'ansico_plugins_reset_defaults');
echo ' <a href="' . esc_url($reset_url) . '" class="button button-secondary" onclick="return confirm(\'' . esc_js(__('Nulstil til Ansico standardindstillinger?', 'ansico-plugins')) . '\');">' . esc_html__('Reset til Ansico standard', 'ansico-plugins') . '</a>';
?>
</form>
<p>
<a href="<?php echo esc_url($test_url); ?>" class="button button-secondary"><?php esc_html_e('Test forbindelse', 'ansico-plugins'); ?></a>
<a href="<?php echo esc_url(admin_url('plugins.php?page=ansico-plugins-catalog')); ?>" class="button button-primary"><?php esc_html_e('Åbn plugin-katalog', 'ansico-plugins'); ?></a>
</p>
<hr />
<h2><?php esc_html_e('Sådan kommer du i gang', 'ansico-plugins'); ?></h2>
<ol>
<li><?php esc_html_e('Indtast base URL til Forgejo, fx https://forgejo.ditdomæne.dk/.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Angiv bruger eller organisation som ejer de repositories, der skal vises.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Opret et access token i Forgejo med læseadgang til repos og releases, hvis repos er private.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Tilføj topic på dine plugin-repositories, fx wordpress-plugin, så kataloget kan filtrere dem.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Upload en installérbar ZIP som release-asset på hver release. Kataloget vælger første ZIP-fil i seneste release.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Installer første gang via Plugins → Ansico Plugins. Derefter kan WordPress selv opdage nye versioner for de plugins, der er installeret via Ansico Plugins.', 'ansico-plugins'); ?></li>
<li><?php esc_html_e('Du kan også slå selvopdatering til for Ansico Plugins via et separat repository under sektionen nedenfor.', 'ansico-plugins'); ?></li>
</ol>
</div>
<?php
}
public function maybe_show_test_notice() {
if (!current_user_can('manage_options') || empty($_GET['ansico_test_connection'])) {
return;
}
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'ansico_plugins_test_connection')) {
return;
}
$client = new Ansico_Plugins_Client();
$result = $client->test_connection();
if (is_wp_error($result)) {
printf(
'<div class="notice notice-error"><p><strong>%s</strong> %s</p></div>',
esc_html__('Forgejo-forbindelse fejlede:', 'ansico-plugins'),
esc_html($result->get_error_message())
);
return;
}
printf(
'<div class="notice notice-success"><p>%s <strong>%s</strong></p></div>',
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) : '';
?>
<div class="wrap ansico-plugins-wrap">
<?php $this->render_catalog_styles(); ?>
<h1 class="wp-heading-inline"><?php esc_html_e('Ansico Plugins', 'ansico-plugins'); ?></h1>
<a href="<?php echo esc_url(admin_url('options-general.php?page=ansico-plugins')); ?>" class="page-title-action"><?php esc_html_e('Indstillinger', 'ansico-plugins'); ?></a>
<hr class="wp-header-end" />
<div class="ansico-directory-header">
<div>
<p><?php printf(
wp_kses(
__('Installer, aktivér, geninstallér, opdatér eller afinstallér dine interne plugins direkte fra Forgejo releases fra <a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s</a>.', 'ansico-plugins'),
array('a' => array('href' => array(), 'target' => array(), 'rel' => array()))
),
esc_url($forgejo_link),
esc_html($owner_label)
); ?></p>
<?php echo $this->get_self_plugin_status_markup($client); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div>
</div>
<?php
if (is_wp_error($repos)) {
printf('<div class="notice notice-error"><p>%s</p></div>', esc_html($repos->get_error_message()));
echo '<p><a class="button" href="' . esc_url(admin_url('options-general.php?page=ansico-plugins')) . '">' . esc_html__('Gå til indstillinger', 'ansico-plugins') . '</a></p>';
echo '</div>';
return;
}
if (empty($repos)) {
echo '<div class="notice notice-info"><p>' . esc_html__('Ingen repositories matchede de valgte kriterier.', 'ansico-plugins') . '</p></div>';
echo '<p>' . esc_html__('Forgejo svarer, men ingen repos matchede topic-filteret. Kontrollér topic-navn og gem indstillingerne igen for at rydde cache.', 'ansico-plugins') . '</p>';
echo '</div>';
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 '<div class="ansico-plugin-grid">';
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 '<div class="ansico-plugin-card">';
echo '<div class="ansico-plugin-card__top">';
echo '<div class="ansico-plugin-icon">AP</div>';
echo '<div class="ansico-plugin-meta">';
echo '<h2><a href="' . esc_url($details_url) . '" class="thickbox">' . esc_html($display_name) . '</a></h2>';
echo '<div class="ansico-plugin-submeta">' . $badge . '</div>';
echo '</div></div>';
echo '<p class="ansico-plugin-description">' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '</p>';
echo '<div class="ansico-plugin-facts">';
echo '<span><strong>' . esc_html__('Installeret', 'ansico-plugins') . ':</strong> ' . esc_html($plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—') . '</span>';
echo '<span><strong>' . esc_html__('Seneste', 'ansico-plugins') . ':</strong> ' . esc_html($version_label) . '</span>';
echo '</div>';
echo '<div class="ansico-plugin-actions">';
foreach ($this->build_action_buttons($repo, $plugin_state, $zip_asset) as $button_html) {
echo $button_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<a class="button-link thickbox" href="' . esc_url($details_url) . '">' . esc_html__('Vis detaljer', 'ansico-plugins') . '</a>';
echo '</div>';
echo $this->get_details_inline_markup($repo, $release, $zip_asset, $plugin_state, $metadata); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
}
echo '</div></div>';
}
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 '<div class="wrap ansico-plugins-wrap">';
$this->render_catalog_styles();
echo '<h1 class="wp-heading-inline">' . esc_html(!empty($metadata['readme_title']) ? $metadata['readme_title'] : $repo['name']) . '</h1>';
echo '<a href="' . esc_url($back_url) . '" class="page-title-action">' . esc_html__('Tilbage til katalog', 'ansico-plugins') . '</a>';
echo '<hr class="wp-header-end" />';
echo $this->get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
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[] = '<a class="button button-primary" href="' . esc_url($this->build_action_url('install_latest', $repo_name, $slug)) . '">' . esc_html__('Installér', 'ansico-plugins') . '</a>';
return $buttons;
}
if ($plugin_state['is_installed'] && !$plugin_state['is_active']) {
$buttons[] = '<a class="button button-primary" href="' . esc_url(wp_nonce_url(admin_url('plugins.php?action=activate&plugin=' . rawurlencode($plugin_state['plugin_file'])), 'activate-plugin_' . $plugin_state['plugin_file'])) . '">' . esc_html__('Aktivér', 'ansico-plugins') . '</a>';
$buttons[] = '<a class="button button-secondary" href="' . esc_url($this->build_action_url('delete_plugin', $repo_name, $slug, $plugin_state['plugin_file'])) . '" onclick="return confirm(\'' . esc_js(__('Er du sikker på, at pluginet skal afinstalleres?', 'ansico-plugins')) . '\');">' . esc_html__('Afinstallér', 'ansico-plugins') . '</a>';
return $buttons;
}
if ($plugin_state['is_installed'] && $plugin_state['has_update']) {
$buttons[] = '<a class="button button-primary" href="' . esc_url($this->build_action_url('install_latest', $repo_name, $slug)) . '">' . esc_html__('Opdatér', 'ansico-plugins') . '</a>';
$buttons[] = '<a class="button button-secondary" href="' . esc_url($this->build_action_url('delete_plugin', $repo_name, $slug, $plugin_state['plugin_file'])) . '" onclick="return confirm(\'' . esc_js(__('Er du sikker på, at pluginet skal afinstalleres?', 'ansico-plugins')) . '\');">' . esc_html__('Afinstallér', 'ansico-plugins') . '</a>';
return $buttons;
}
if ($plugin_state['is_installed'] && $plugin_state['is_active']) {
$buttons[] = '<a class="button button-primary" href="' . esc_url($this->build_action_url('install_latest', $repo_name, $slug)) . '">' . esc_html__('Geninstallér', 'ansico-plugins') . '</a>';
$buttons[] = '<a class="button button-secondary" href="' . esc_url($this->build_action_url('delete_plugin', $repo_name, $slug, $plugin_state['plugin_file'])) . '" onclick="return confirm(\'' . esc_js(__('Er du sikker på, at pluginet skal afinstalleres?', 'ansico-plugins')) . '\');">' . esc_html__('Afinstallér', 'ansico-plugins') . '</a>';
}
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 '<span class="ansico-badge ansico-badge--neutral">' . esc_html__('Ikke udgivet', 'ansico-plugins') . '</span>';
}
return '<span class="ansico-badge ansico-badge--neutral">' . esc_html__('Ikke installeret', 'ansico-plugins') . '</span>';
}
if ($plugin_state['has_update']) {
return '<span class="ansico-badge ansico-badge--warning">' . esc_html__('Opdatering klar', 'ansico-plugins') . '</span>';
}
if (!$plugin_state['is_active']) {
return '<span class="ansico-badge ansico-badge--muted">' . esc_html__('Installeret', 'ansico-plugins') . '</span>';
}
return '<span class="ansico-badge ansico-badge--success">' . esc_html__('Aktiv', 'ansico-plugins') . '</span>';
}
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 '<p class="ansico-self-status ansico-self-status--error">' . esc_html__('Ansico Plugins: kunne ikke hente opdateringsstatus.', 'ansico-plugins') . '</p>';
}
$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 '<p class="ansico-self-status">' . esc_html__('Ansico Plugins: ingen udgivet version fundet endnu.', 'ansico-plugins') . '</p>';
}
if (version_compare($remote_version, $current_version, '>')) {
$update_url = esc_url(admin_url('plugins.php'));
return '<p class="ansico-self-status ansico-self-status--warning">' . sprintf(
wp_kses(
__('Ansico Plugins: version %1$s er installeret, og version %2$s er tilgængelig fra <a href="%3$s" target="_blank" rel="noopener noreferrer">%4$s</a>. Opdater fra den almindelige <a href="%5$s">Plugins-side</a>.', '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)
) . '</p>';
}
return '<p class="ansico-self-status ansico-self-status--success">' . sprintf(
wp_kses(
__('Ansico Plugins er opdateret. Installeret version %1$s matcher seneste release i <a href="%2$s" target="_blank" rel="noopener noreferrer">%3$s</a>.', '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'])
) . '</p>';
}
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 '<div id="' . esc_attr($id) . '" style="display:none;">' .
'<div class="ansico-thickbox-content">' .
$this->get_details_content_markup($repo, $release, $zip_asset, $plugin_state, $metadata) .
'</div></div>';
}
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 .= '<div class="ansico-plugin-hero">';
$html .= '<div class="ansico-plugin-icon">AP</div>';
$html .= '<div>';
$html .= '<h1 style="margin:0 0 8px;font-size:26px;line-height:1.2">' . esc_html($display_name) . '</h1>';
$html .= '<p class="description" style="margin:0 0 10px">' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '</p>';
$html .= $this->get_state_badge($plugin_state);
$html .= '</div></div>';
$html .= '<div class="ansico-modal-actions">';
foreach ($this->build_action_buttons($repo, $plugin_state, $zip_asset) as $button_html) {
$html .= $button_html;
}
$html .= '<a class="button" href="' . esc_url($repo['html_url']) . '" target="_blank" rel="noopener noreferrer">' . esc_html__('Åbn repository', 'ansico-plugins') . '</a>';
$html .= '</div>';
$html .= '<div class="ansico-meta-grid">';
$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 .= '<p><strong>' . esc_html($label) . '</strong><br>';
if (is_string($value) && preg_match('#^https?://#i', $value)) {
$html .= '<a href="' . esc_url($value) . '" target="_blank" rel="noopener noreferrer">' . esc_html($value) . '</a>';
} else {
$html .= esc_html($value);
}
$html .= '</p>';
}
$html .= '</div>';
if (!empty($metadata['screenshot_url'])) {
$html .= '<div class="ansico-screenshot" style="margin:22px 0">';
$html .= '<h2>' . esc_html__('Screenshot', 'ansico-plugins') . '</h2>';
$html .= '<img src="' . esc_url($metadata['screenshot_url']) . '" alt="' . esc_attr(sprintf(__('Screenshot for %s', 'ansico-plugins'), $display_name)) . '" />';
$html .= '</div>';
}
if (!is_wp_error($release) && !empty($release['body'])) {
$html .= '<h2>' . esc_html__('Release-noter', 'ansico-plugins') . '</h2>';
$html .= '<div class="ansico-release-notes">' . wp_kses_post(wpautop($release['body'])) . '</div>';
}
return $html;
}
private function render_catalog_styles() {
echo '<style>
.ansico-directory-header{display:flex;justify-content:space-between;align-items:center;margin:16px 0 20px}
.ansico-directory-header p{margin:0;color:#50575e;font-size:14px}
.ansico-self-status{margin-top:10px !important;font-size:13px;color:#50575e}
.ansico-self-status--success{color:#008a20}
.ansico-self-status--warning{color:#8a6d00}
.ansico-self-status--error{color:#b32d2e}
.ansico-plugin-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:20px;margin-top:18px}
.ansico-plugin-card{background:#fff;border:1px solid #dcdcde;border-radius:10px;padding:18px;box-shadow:0 1px 2px rgba(0,0,0,.04);display:flex;flex-direction:column;gap:14px}
.ansico-plugin-card__top{display:flex;gap:14px;align-items:flex-start}
.ansico-plugin-icon{width:64px;height:64px;border-radius:12px;background:#2271b1;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:20px;flex-shrink:0}
.ansico-plugin-meta h2{margin:0 0 6px;font-size:18px;line-height:1.3}
.ansico-plugin-meta h2 a{text-decoration:none}
.ansico-plugin-submeta{display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:#646970}
.ansico-plugin-description{margin:0;color:#1d2327;min-height:48px}
.ansico-plugin-facts{display:grid;grid-template-columns:1fr;gap:6px;font-size:13px;color:#50575e}
.ansico-plugin-actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:auto}
.ansico-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;font-size:12px;font-weight:600}
.ansico-badge--neutral{background:#f0f0f1;color:#3c434a}
.ansico-badge--muted{background:#f6f7f7;color:#50575e}
.ansico-badge--success{background:#edfaef;color:#008a20}
.ansico-badge--warning{background:#fcf9e8;color:#8a6d00}
.ansico-plugin-hero{display:flex;gap:16px;align-items:flex-start;margin-bottom:18px}
.ansico-plugin-hero .ansico-plugin-icon{width:72px;height:72px;font-size:22px}
.ansico-meta-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px 20px;margin:18px 0}
.ansico-meta-grid p{margin:0}
.ansico-screenshot img{max-width:100%;height:auto;border:1px solid #dcdcde;border-radius:8px}
.ansico-modal-actions{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}
.ansico-release-notes{background:#f6f7f7;border-radius:8px;padding:14px}
.ansico-thickbox-content{padding:20px 24px 24px}
#TB_window{max-height:81vh !important}#TB_ajaxContent{max-height:calc(81vh - 45px) !important;height:auto !important;overflow:auto !important}#TB_ajaxContent .ansico-thickbox-content{width:auto !important;height:auto !important;max-height:calc(81vh - 65px) !important;overflow:auto !important;padding:20px 24px 24px !important;box-sizing:border-box}
@media (max-width: 960px){.ansico-meta-grid{grid-template-columns:1fr}}
</style>';
}
}

View file

@ -0,0 +1,463 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class Ansico_Plugins_Client {
private $settings;
public function __construct($settings = array()) {
$this->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;
}
}

View file

@ -0,0 +1,192 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class Ansico_Plugins_Installer {
public function install_latest_release($repo_name, $slug) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$client = new Ansico_Plugins_Client();
$settings = Ansico_Plugins::get_settings();
$owner = $settings['forgejo_owner'];
$release = $client->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 '<div class="wrap">';
echo '<h1>' . esc_html(sprintf($is_update ? __('Opdaterer %s', 'ansico-plugins') : __('Installerer %s', 'ansico-plugins'), $repo_name)) . '</h1>';
$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 '<div class="notice notice-error"><p>' . esc_html($message) . '</p></div>';
echo '<p><a class="button" href="' . esc_url(admin_url('plugins.php?page=ansico-plugins-catalog')) . '">' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '</a></p>';
echo '</div>';
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 '<div class="notice notice-success"><p>' . esc_html($is_update ? __('Plugin opdateret.', 'ansico-plugins') : __('Plugin installeret.', 'ansico-plugins')) . '</p></div>';
echo '<p><a class="button button-secondary" href="' . esc_url(admin_url('plugins.php?page=ansico-plugins-catalog')) . '">' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '</a> ';
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 '<a class="button button-primary" href="' . esc_url($activate_url) . '">' . esc_html__('Aktivér plugin', 'ansico-plugins') . '</a> ';
}
echo '<a class="button" href="' . esc_url(admin_url('plugins.php')) . '">' . esc_html__('Gå til Plugins', 'ansico-plugins') . '</a></p>';
echo '</div>';
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 '<div class="wrap">';
echo '<h1>' . esc_html(sprintf(__('Afinstallerer %s', 'ansico-plugins'), $repo_name)) . '</h1>';
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 '<div class="notice notice-error"><p>' . esc_html($message) . '</p></div>';
echo '<p><a class="button" href="' . esc_url(admin_url('plugins.php?page=ansico-plugins-catalog')) . '">' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '</a></p>';
echo '</div>';
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 '<div class="notice notice-success"><p>' . esc_html__('Plugin afinstalleret.', 'ansico-plugins') . '</p></div>';
echo '<p><a class="button button-primary" href="' . esc_url(admin_url('plugins.php?page=ansico-plugins-catalog')) . '">' . esc_html__('Tilbage til Ansico Plugins', 'ansico-plugins') . '</a></p>';
echo '</div>';
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;
}
}

View file

@ -0,0 +1,307 @@
<?php
if (!defined('ABSPATH')) {
exit;
}
class Ansico_Plugins_Updater {
public function __construct() {
add_filter('pre_set_site_transient_update_plugins', array($this, 'inject_update_data'));
add_filter('plugins_api', array($this, 'plugins_api'), 20, 3);
add_filter('http_request_args', array($this, 'inject_auth_header'), 10, 2);
}
public function inject_update_data($transient) {
if (!is_object($transient)) {
$transient = new stdClass();
}
if (empty($transient->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']) ? '<a href="' . esc_url(!empty($self_info['repo_meta']['author_url']) ? $self_info['repo_meta']['author_url'] : 'https://ansico.dk') . '">' . esc_html($self_info['repo_meta']['author_name']) . '</a>' : '<a href="https://ansico.dk">Andreas Andersen (Ansico)</a>',
'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']) ? '<a href="' . esc_url(!empty($repo_meta['author_url']) ? $repo_meta['author_url'] : 'https://ansico.dk') . '">' . esc_html($repo_meta['author_name']) . '</a>' : '<a href="https://ansico.dk">Andreas Andersen (Ansico)</a>',
'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);
}
}

56
ansico-plugins/readme.txt Normal file
View file

@ -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.