commit 52f965c9c602e5155fa7a5a2d35071ba241c5024 Author: roberto Date: Sun Feb 1 18:10:28 2026 +0100 Initial commit diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..587109b --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,25 @@ +name: Build & Release Plugin + +on: + workflow_dispatch: + inputs: + release_notes: + description: "Optionele release-opmerkingen" + required: false + push: + branches: [ main ] + paths: + - 'master-file.php' + - 'includes/**' + - 'assets/**' + - 'languages/**' + +jobs: + release: + uses: roberto/ci-workflows/.gitea/workflows/wp-plugin-release.yml@c6393ed47258d6f040ceeed3994b17b7faa3ef23 + with: + main_file: master-file.php + slug: siti-plugin-template + release_body: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_notes || '' }} + secrets: + RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/includes/SitiLicenseValidator.php b/includes/SitiLicenseValidator.php new file mode 100644 index 0000000..baadd8f --- /dev/null +++ b/includes/SitiLicenseValidator.php @@ -0,0 +1,181 @@ +license_key = $license_key ?: get_option( 'siti_license_key' ); + $this->hostname = $hostname ?: parse_url( home_url(), PHP_URL_HOST ); + $this->plugin_version = $plugin_version; + } + + /** + * Set the license key + */ + public function set_license_key( $key ) { + $this->license_key = $key; + } + + /** + * Set the API base URL + */ + public function set_api_base_url( $url ) { + $this->api_base_url = rtrim( $url, '/' ); + } + + /** + * Set the hostname + */ + public function set_hostname( $hostname ) { + $this->hostname = $hostname; + } + + /** + * Set the plugin version for reporting purposes + */ + public function set_plugin_version( $version ) { + $this->plugin_version = $version; + } + + /** + * Verify the license + * + * @return array|WP_Error License data on success, WP_Error on failure + */ + public function verify() { + if ( empty( $this->license_key ) ) { + return new WP_Error( 'missing_license', 'Geen licentiecode ingesteld.' ); + } + + if ( empty( $this->hostname ) ) { + return new WP_Error( 'missing_hostname', 'Kon hostname niet bepalen.' ); + } + + $response = wp_remote_post( $this->api_base_url . '/api/licenses/verify', array( + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( array( + 'key' => $this->license_key, + 'hostname' => $this->hostname, + 'currentVersion' => $this->plugin_version, + ) ), + 'timeout' => 15, + ) ); + + if ( is_wp_error( $response ) ) { + return $response; + } + + $code = wp_remote_retrieve_response_code( $response ); + $body = json_decode( wp_remote_retrieve_body( $response ), true ); + + if ( ! is_array( $body ) ) { + return new WP_Error( 'verification_failed', 'Onverwacht antwoord van licentieserver.' ); + } + + $has_payload = isset( $body['license'] ) || isset( $body['pluginVersion'] ) || isset( $body['version'] ); + + if ( 200 !== $code ) { + if ( $has_payload ) { + $this->store_license_payload( $body ); + } + + $error_message = isset( $body['error'] ) ? $body['error'] : 'Licentie verificatie mislukt.'; + return new WP_Error( 'verification_failed', $error_message ); + } + + if ( empty( $body['valid'] ) ) { + if ( $has_payload ) { + $this->store_license_payload( $body ); + } + + return new WP_Error( 'invalid_license', 'Licentie is ongeldig.' ); + } + + $this->store_license_payload( $body ); + + return $body; + } + + /** + * Get stored license data + */ + public function get_license_data() { + return get_option( 'siti_license_data', array() ); + } + + /** + * Check if license is valid (cached) + */ + public function is_valid() { + $data = $this->get_license_data(); + return ! empty( $data ) && isset( $data['key'] ); + } + + /** + * Get the latest version from license data + */ + public function get_latest_version() { + $data = $this->get_license_data(); + return $data['pluginVersion'] ?? null; + } + + /** + * Download a specific version + * + * @param string $version Version to download (e.g., 'v1.2.3' or 'latest') + * @return string|WP_Error Download URL or error + */ + public function get_download_url( $version = 'latest' ) { + if ( empty( $this->license_key ) || empty( $this->hostname ) ) { + return new WP_Error( 'missing_credentials', 'Licentie of hostname ontbreekt.' ); + } + + // For now, return the API download endpoint + // In practice, you might want to proxy this through WordPress + return $this->api_base_url . '/api/licenses/download?key=' . urlencode( $this->license_key ) . '&hostname=' . urlencode( $this->hostname ) . '&version=' . urlencode( $version ); + } + + /** + * Persist payload data locally for UI purposes + */ + private function store_license_payload( $body ) { + $license_data = array(); + + if ( isset( $body['license'] ) && is_array( $body['license'] ) ) { + $license_data = $body['license']; + } + + if ( empty( $license_data['key'] ) ) { + $license_data['key'] = $this->license_key; + } + + $version = null; + if ( ! empty( $body['pluginVersion'] ) ) { + $version = $body['pluginVersion']; + } elseif ( ! empty( $body['version'] ) ) { + $version = $body['version']; + } + + if ( $version ) { + $license_data['pluginVersion'] = $version; + } + + if ( empty( $license_data ) ) { + return; + } + + update_option( 'siti_license_data', $license_data ); + update_option( 'siti_license_last_check', current_time( 'mysql' ) ); + } +} diff --git a/includes/SitiWebUpdater2.php b/includes/SitiWebUpdater2.php new file mode 100644 index 0000000..7756606 --- /dev/null +++ b/includes/SitiWebUpdater2.php @@ -0,0 +1,510 @@ +file = $file; + $this->option_prefix = 'siti_updater_' . sanitize_key(dirname(plugin_basename($this->file))) . '_'; + $this->set_plugin_properties(); + + // Register this instance globally + self::$instances[$this->option_prefix] = $this; + + add_action('admin_init', array($this, 'set_plugin_properties')); + add_action('admin_notices', array($this, 'admin_notices')); + add_action('siti_updater_daily_check_' . $this->option_prefix, array($this, 'daily_license_check')); + + if (!wp_next_scheduled('siti_updater_daily_check_' . $this->option_prefix)) { + wp_schedule_event(time(), 'daily', 'siti_updater_daily_check_' . $this->option_prefix); + } + + return $this; + } + + public function initialize() + { + add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates')); + add_filter('plugins_api', array($this, 'plugin_info'), 10, 3); + + // Register global settings page only once + if (!self::$global_settings_registered) { + add_action('admin_menu', array($this, 'add_global_settings_page')); + self::$global_settings_registered = true; + } + } + + public function set_plugin_properties() + { + if (!function_exists('get_plugin_data')) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + + $this->plugin = get_plugin_data($this->file, false, false); + $this->basename = plugin_basename($this->file); + $this->active = is_plugin_active($this->basename); + } + + public static function get_instances() + { + return self::$instances; + } + + public function get_plugin_data() + { + return $this->plugin; + } + + public function set_owner($owner) + { + $this->owner = $owner; + } + + public function set_repository($repository) + { + $this->repository = $repository; + } + + public function set_license_key($key) + { + $sanitized_key = sanitize_text_field((string) $key); + $stored_key = get_option($this->option_prefix . 'license_key', ''); + + if ($stored_key !== $sanitized_key) { + $this->clear_cached_license_data(); + } + + $this->license_key = $sanitized_key; + update_option($this->option_prefix . 'license_key', $sanitized_key); + } + + public function get_license_key() + { + if (null === $this->license_key) { + $this->license_key = get_option($this->option_prefix . 'license_key', ''); + } + return $this->license_key; + } + + public function get_license_data() + { + return $this->get_stored_license_data(); + } + + public function set_api_base_url($api_base_url) + { + $this->api_base_url = rtrim((string) $api_base_url, '/'); + } + + private function normalize_version($version) + { + if (!is_string($version)) { + return $version; + } + + $version = trim($version); + if ('' === $version) { + return $version; + } + + if (0 === stripos($version, 'v')) { + $version = ltrim($version, 'vV'); + } + + return $version; + } + + private function get_stored_license_data() + { + $data = get_option($this->option_prefix . 'license_data', array()); + + return is_array($data) ? $data : array(); + } + + private function clear_cached_license_data() + { + delete_option($this->option_prefix . 'license_data'); + delete_option($this->option_prefix . 'last_check'); + } + + private function store_license_data($body) + { + if (!is_array($body)) { + $body = array(); + } + + $license_data = array(); + + if (isset($body['license']) && is_array($body['license'])) { + $license_data = $body['license']; + } + + if (empty($license_data['key'])) { + $license_data['key'] = $this->get_license_key(); + } + + $license_data['licenseValid'] = !empty($body['valid']); + + $version = null; + if (isset($body['pluginVersion']) && $body['pluginVersion']) { + $version = $body['pluginVersion']; + } elseif (isset($body['version']) && $body['version']) { + $version = $body['version']; + } elseif (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) { + $version = $license_data['pluginVersion']; + } + + if ($version) { + $license_data['pluginVersion'] = $this->normalize_version($version); + } + + update_option($this->option_prefix . 'license_data', $license_data); + update_option($this->option_prefix . 'last_check', current_time('mysql')); + } + + private function get_plugin_info() + { + if (is_null($this->plugin_response)) { + $request_uri = sprintf('%s/api/plugins/%s/%s', $this->api_base_url, $this->owner, $this->repository); + + $response = wp_remote_get($request_uri, array('timeout' => 15)); + + if (is_wp_error($response)) { + $this->plugin_response = false; + return; + } + + $code = wp_remote_retrieve_response_code($response); + if (200 !== $code) { + $this->plugin_response = false; + return; + } + + $body = json_decode(wp_remote_retrieve_body($response), true); + if (is_array($body) && isset($body['version'])) { + $body['version'] = $this->normalize_version($body['version']); + } + $this->plugin_response = $body; + } + } + + public function check_for_updates($transient) + { + if (empty($transient->checked)) { + return $transient; + } + + $this->get_plugin_info(); + + if (false === $this->plugin_response || empty($this->plugin_response['version'])) { + return $transient; + } + + $remote_version = $this->normalize_version($this->plugin_response['version']); + $current_version = $this->normalize_version($this->plugin['Version']); + + if ($remote_version && version_compare($remote_version, $current_version, '>')) { + $download_url = $this->get_download_url('latest'); + + if (!is_wp_error($download_url)) { + $transient->response[$this->basename] = (object) array( + 'slug' => dirname($this->basename), + 'new_version' => $remote_version, + 'package' => $download_url, + 'tested' => get_bloginfo('version'), + 'url' => $this->plugin_response['homepage'] ?? '', + ); + } + } + + return $transient; + } + + public function plugin_info($result, $action, $args) + { + if ('plugin_information' !== $action || $args->slug !== dirname($this->basename)) { + return $result; + } + + $this->get_plugin_info(); + + if (false === $this->plugin_response) { + return $result; + } + + return (object) array( + 'name' => $this->plugin_response['name'] ?? $this->plugin['Name'], + 'slug' => $args->slug, + 'version' => $this->normalize_version($this->plugin_response['version'] ?? $this->plugin['Version']), + 'author' => $this->plugin_response['author'] ?? $this->plugin['Author'], + 'author_profile' => $this->plugin_response['author_url'] ?? $this->plugin['AuthorURI'], + 'homepage' => $this->plugin_response['homepage'] ?? '', + 'requires' => $this->plugin_response['requires'] ?? '', + 'tested' => $this->plugin_response['tested'] ?? get_bloginfo('version'), + 'downloaded' => $this->plugin_response['downloads'] ?? 0, + 'last_updated' => $this->plugin_response['last_updated'] ?? '', + 'sections' => array( + 'description' => $this->plugin_response['description'] ?? $this->plugin['Description'], + 'changelog' => $this->plugin_response['changelog'] ?? '', + ), + 'download_link' => $this->get_download_url('latest'), + ); + } + + private function get_download_url($version = 'latest') + { + $license_key = $this->get_license_key(); + if (empty($license_key)) { + return new WP_Error('no_license', 'Geen licentie ingesteld'); + } + + $hostname = parse_url(home_url(), PHP_URL_HOST); + return $this->api_base_url . '/api/licenses/download?key=' . urlencode($license_key) . '&hostname=' . urlencode($hostname) . '&version=' . urlencode($version); + } + + public function verify_license() + { + $license_key = $this->get_license_key(); + if (empty($license_key)) { + return new WP_Error('missing_license', 'Geen licentiecode ingesteld.'); + } + + $hostname = parse_url(home_url(), PHP_URL_HOST); + $current_version = isset($this->plugin['Version']) ? $this->plugin['Version'] : null; + $response = wp_remote_post($this->api_base_url . '/api/licenses/verify', array( + 'headers' => array( + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode(array( + 'key' => $license_key, + 'hostname' => $hostname, + 'currentVersion' => $current_version, + 'owner' => $this->owner, + 'repository' => $this->repository, + )), + 'timeout' => 15, + )); + + if (is_wp_error($response)) { + return $response; + } + + $code = wp_remote_retrieve_response_code($response); + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (!is_array($body)) { + $this->clear_cached_license_data(); + return new WP_Error('verification_failed', 'Onverwacht antwoord van licentieserver.'); + } + + $has_license_payload = isset($body['license']) || isset($body['pluginVersion']) || isset($body['version']); + + if (200 !== $code) { + if ($has_license_payload) { + $this->store_license_data($body); + } else { + $this->clear_cached_license_data(); + } + + $error_message = isset($body['error']) ? $body['error'] : 'Licentie verificatie mislukt.'; + return new WP_Error('verification_failed', $error_message); + } + + if (empty($body['valid'])) { + if ($has_license_payload) { + $this->store_license_data($body); + } else { + $this->clear_cached_license_data(); + } + + return new WP_Error('invalid_license', 'Licentie is ongeldig.'); + } + + $this->store_license_data($body); + + return $body; + } + + public function is_license_valid() + { + $data = $this->get_license_data(); + if (empty($data) || empty($data['key'])) { + return false; + } + + if (isset($data['licenseValid'])) { + return (bool) $data['licenseValid']; + } + + return true; + } + + public function get_latest_known_version() + { + $this->get_plugin_info(); + + if (!empty($this->plugin_response['version'])) { + return $this->normalize_version($this->plugin_response['version']); + } + + $license_data = $this->get_license_data(); + if (isset($license_data['pluginVersion']) && $license_data['pluginVersion']) { + return $this->normalize_version($license_data['pluginVersion']); + } + + return null; + } + + public function admin_notices() + { + if (!current_user_can('manage_options')) { + return; + } + + if (!$this->is_license_valid()) { + $settings_url = admin_url('options-general.php?page=siti-plugin-licenses'); + echo '

'; + printf( + esc_html__('%s: Licentie ongeldig of niet ingesteld. Ga naar %s om je licentie in te voeren.', 'siti-updater'), + esc_html($this->plugin['Name']), + '' . esc_html__('plugin licenties', 'siti-updater') . '' + ); + echo '

'; + } + } + + public function daily_license_check() + { + $result = $this->verify_license(); + if (is_wp_error($result)) { + error_log($this->plugin['Name'] . ' License check failed: ' . $result->get_error_message()); + } + } + + public function add_global_settings_page() + { + add_options_page( + __('Plugin Licenties', 'siti-updater'), + __('Plugin Licenties', 'siti-updater'), + 'manage_options', + 'siti-plugin-licenses', + array($this, 'render_global_settings_page') + ); + } + + public function render_global_settings_page() + { + if (!current_user_can('manage_options')) { + return; + } + + if (isset($_POST['siti_updater_save']) && wp_verify_nonce($_POST['_wpnonce'], 'siti_updater_save')) { + foreach (self::$instances as $prefix => $instance) { + $key = $prefix . 'license_key'; + if (isset($_POST[$key])) { + $instance->set_license_key(sanitize_text_field($_POST[$key])); + $result = $instance->verify_license(); + if (is_wp_error($result)) { + $plugin_data = $instance->get_plugin_data(); + add_settings_error('siti_updater', 'license_error_' . $prefix, $plugin_data['Name'] . ': ' . $result->get_error_message()); + } else { + $plugin_data = $instance->get_plugin_data(); + add_settings_error('siti_updater', 'license_success_' . $prefix, $plugin_data['Name'] . ': ' . __('Licentie succesvol opgeslagen en geverifieerd.', 'siti-updater'), 'updated'); + } + } + } + } + + ?> +
+

+

+ +

+ + + +
+ + + + + + + + + + + + + $instance): + $license_data = $instance->get_license_data(); + $is_valid = $instance->is_license_valid(); + $plugin_data = $instance->get_plugin_data(); + $current_version = $plugin_data['Version']; + $latest_known = $instance->get_latest_known_version(); + $latest_version = $latest_known ? $latest_known : (isset($license_data['pluginVersion']) ? $license_data['pluginVersion'] : '-'); + ?> + + + + + + + + + +
+ +
+ +
+ +

+ +

+
+ + ✓ ' . esc_html__('Ja', 'siti-updater') . '' : '✗ ' . esc_html__('Nee', 'siti-updater') . ''; ?> + + + + + + ')): ?> +
+ +
+

+ +

+
+
+ 'Version', + ], + false + ); + + $plugin_version = isset( $plugin_data['Version'] ) && $plugin_data['Version'] ? $plugin_data['Version'] : '1.0.0'; + define( 'PLUGIN_SLUG_VERSION', $plugin_version ); +} + +if( ! class_exists( 'SitiWebUpdater2' ) ){ + include_once( plugin_dir_path( __FILE__ ) . 'includes/SitiWebUpdater2.php' ); +} + +$updater = new SitiWebUpdater2( __FILE__ ); +$updater->set_owner( 'roberto' ); +$updater->set_repository( 'siti-plugin-template' ); +$updater->initialize(); \ No newline at end of file