474 lines
12 KiB
PHP
474 lines
12 KiB
PHP
|
|
<?php
|
||
|
|
/**
|
||
|
|
* Plugin Updater class.
|
||
|
|
*
|
||
|
|
* Integrates with WordPress plugin update system to check for and install
|
||
|
|
* updates from the license server.
|
||
|
|
*
|
||
|
|
* @package Magdev\WpBnb\License
|
||
|
|
*/
|
||
|
|
|
||
|
|
declare( strict_types=1 );
|
||
|
|
|
||
|
|
namespace Magdev\WpBnb\License;
|
||
|
|
|
||
|
|
use Magdev\WcLicensedProductClient\SecureLicenseClient;
|
||
|
|
use Magdev\WcLicensedProductClient\LicenseClient;
|
||
|
|
use Magdev\WcLicensedProductClient\Dto\UpdateInfo;
|
||
|
|
use Symfony\Component\HttpClient\HttpClient;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handles plugin auto-updates from the license server.
|
||
|
|
*/
|
||
|
|
final class Updater {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Singleton instance.
|
||
|
|
*
|
||
|
|
* @var Updater|null
|
||
|
|
*/
|
||
|
|
private static ?Updater $instance = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Plugin basename (e.g., wp-bnb/wp-bnb.php).
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private string $plugin_basename;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Plugin slug.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private string $plugin_slug;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Current plugin version.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private string $current_version;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache key for update info.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private const CACHE_KEY = 'wp_bnb_update_info';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cache key for last check timestamp.
|
||
|
|
*
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
private const LAST_CHECK_KEY = 'wp_bnb_update_last_check';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Default cache duration in seconds (12 hours).
|
||
|
|
*
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
private const DEFAULT_CHECK_FREQUENCY = 12;
|
||
|
|
|
||
|
|
// Option keys for update settings.
|
||
|
|
public const OPTION_NOTIFICATIONS_ENABLED = 'wp_bnb_update_notifications_enabled';
|
||
|
|
public const OPTION_AUTO_INSTALL_ENABLED = 'wp_bnb_auto_install_enabled';
|
||
|
|
public const OPTION_CHECK_FREQUENCY = 'wp_bnb_update_check_frequency';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* License client instance.
|
||
|
|
*
|
||
|
|
* @var SecureLicenseClient|LicenseClient|null
|
||
|
|
*/
|
||
|
|
private SecureLicenseClient|LicenseClient|null $client = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Constructor.
|
||
|
|
*
|
||
|
|
* @param string $plugin_file Full path to the main plugin file.
|
||
|
|
* @param string $current_version Current plugin version.
|
||
|
|
*/
|
||
|
|
public function __construct( string $plugin_file, string $current_version ) {
|
||
|
|
$this->plugin_basename = plugin_basename( $plugin_file );
|
||
|
|
$this->plugin_slug = dirname( $this->plugin_basename );
|
||
|
|
$this->current_version = $current_version;
|
||
|
|
self::$instance = $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the singleton instance.
|
||
|
|
*
|
||
|
|
* @return Updater|null
|
||
|
|
*/
|
||
|
|
public static function get_instance(): ?Updater {
|
||
|
|
return self::$instance;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize update hooks.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function init(): void {
|
||
|
|
// Allow complete disable via constant.
|
||
|
|
if ( defined( 'WP_BNB_DISABLE_AUTO_UPDATE' ) && WP_BNB_DISABLE_AUTO_UPDATE ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Hook into WordPress update system.
|
||
|
|
add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_for_updates' ) );
|
||
|
|
add_filter( 'plugins_api', array( $this, 'plugin_info' ), 10, 3 );
|
||
|
|
add_action( 'upgrader_process_complete', array( $this, 'after_update' ), 10, 2 );
|
||
|
|
|
||
|
|
// Auto-install filter for WordPress background updates.
|
||
|
|
add_filter( 'auto_update_plugin', array( $this, 'auto_update_plugin' ), 10, 2 );
|
||
|
|
|
||
|
|
// Clear update cache when license settings change.
|
||
|
|
add_action( 'update_option_' . Manager::OPTION_LICENSE_KEY, array( $this, 'clear_cache' ) );
|
||
|
|
add_action( 'update_option_' . Manager::OPTION_SERVER_URL, array( $this, 'clear_cache' ) );
|
||
|
|
|
||
|
|
// AJAX handler for manual update check.
|
||
|
|
add_action( 'wp_ajax_wp_bnb_check_updates', array( $this, 'ajax_check_updates' ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if update notifications are enabled.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public static function is_notifications_enabled(): bool {
|
||
|
|
return 'yes' === get_option( self::OPTION_NOTIFICATIONS_ENABLED, 'yes' );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if auto-install is enabled.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
public static function is_auto_install_enabled(): bool {
|
||
|
|
return 'yes' === get_option( self::OPTION_AUTO_INSTALL_ENABLED, 'no' );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the update check frequency in hours.
|
||
|
|
*
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public static function get_check_frequency(): int {
|
||
|
|
$frequency = (int) get_option( self::OPTION_CHECK_FREQUENCY, self::DEFAULT_CHECK_FREQUENCY );
|
||
|
|
// Clamp between 1 and 168 hours (1 week).
|
||
|
|
return max( 1, min( 168, $frequency ) );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get cache duration in seconds based on check frequency.
|
||
|
|
*
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
private function get_cache_duration(): int {
|
||
|
|
return self::get_check_frequency() * 3600;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Filter for WordPress auto-update system.
|
||
|
|
*
|
||
|
|
* @param bool|null $update Whether to update the plugin.
|
||
|
|
* @param object $item The plugin update object.
|
||
|
|
* @return bool|null
|
||
|
|
*/
|
||
|
|
public function auto_update_plugin( $update, object $item ) {
|
||
|
|
// Only affect our plugin.
|
||
|
|
if ( ! isset( $item->plugin ) || $item->plugin !== $this->plugin_basename ) {
|
||
|
|
return $update;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if auto-install is enabled and license is valid.
|
||
|
|
if ( self::is_auto_install_enabled() && Manager::is_license_valid() ) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $update;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current plugin version.
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function get_current_version(): string {
|
||
|
|
return $this->current_version;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get last update check timestamp.
|
||
|
|
*
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public static function get_last_check(): int {
|
||
|
|
return (int) get_option( self::LAST_CHECK_KEY, 0 );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* AJAX handler: Check for updates.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function ajax_check_updates(): void {
|
||
|
|
check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' );
|
||
|
|
|
||
|
|
if ( ! current_user_can( 'update_plugins' ) ) {
|
||
|
|
wp_send_json_error( array(
|
||
|
|
'message' => __( 'You do not have permission to check for updates.', 'wp-bnb' ),
|
||
|
|
) );
|
||
|
|
}
|
||
|
|
|
||
|
|
$update_info = $this->get_cached_update_info( true );
|
||
|
|
|
||
|
|
if ( null === $update_info ) {
|
||
|
|
wp_send_json_success( array(
|
||
|
|
'update_available' => false,
|
||
|
|
'current_version' => $this->current_version,
|
||
|
|
'message' => __( 'Could not check for updates. Please verify your license configuration.', 'wp-bnb' ),
|
||
|
|
) );
|
||
|
|
}
|
||
|
|
|
||
|
|
$response = array(
|
||
|
|
'update_available' => $update_info->updateAvailable && version_compare( $this->current_version, $update_info->version ?? '', '<' ),
|
||
|
|
'current_version' => $this->current_version,
|
||
|
|
'latest_version' => $update_info->version ?? $this->current_version,
|
||
|
|
'last_check' => time(),
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( $response['update_available'] ) {
|
||
|
|
$response['message'] = sprintf(
|
||
|
|
/* translators: %s: New version number */
|
||
|
|
__( 'A new version (%s) is available.', 'wp-bnb' ),
|
||
|
|
$update_info->version
|
||
|
|
);
|
||
|
|
$response['changelog'] = $update_info->changelog ?? '';
|
||
|
|
} else {
|
||
|
|
$response['message'] = __( 'You are running the latest version.', 'wp-bnb' );
|
||
|
|
}
|
||
|
|
|
||
|
|
wp_send_json_success( $response );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initialize the license client.
|
||
|
|
*
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
private function init_client(): bool {
|
||
|
|
if ( null !== $this->client ) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
$server_url = Manager::get_server_url();
|
||
|
|
$server_secret = Manager::get_server_secret();
|
||
|
|
|
||
|
|
if ( empty( $server_url ) ) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
if ( ! empty( $server_secret ) ) {
|
||
|
|
$this->client = new SecureLicenseClient(
|
||
|
|
httpClient: HttpClient::create(),
|
||
|
|
baseUrl: $server_url,
|
||
|
|
serverSecret: $server_secret,
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
$this->client = new LicenseClient(
|
||
|
|
httpClient: HttpClient::create(),
|
||
|
|
baseUrl: $server_url,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} catch ( \Throwable $e ) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check for plugin updates.
|
||
|
|
*
|
||
|
|
* @param object $transient The update_plugins transient.
|
||
|
|
* @return object Modified transient.
|
||
|
|
*/
|
||
|
|
public function check_for_updates( object $transient ): object {
|
||
|
|
if ( empty( $transient->checked ) ) {
|
||
|
|
return $transient;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Respect notifications enabled setting.
|
||
|
|
if ( ! self::is_notifications_enabled() ) {
|
||
|
|
return $transient;
|
||
|
|
}
|
||
|
|
|
||
|
|
$update_info = $this->get_update_info();
|
||
|
|
|
||
|
|
if ( null === $update_info || ! $update_info->updateAvailable ) {
|
||
|
|
return $transient;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Compare versions.
|
||
|
|
if ( version_compare( $this->current_version, $update_info->version ?? '', '>=' ) ) {
|
||
|
|
return $transient;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add to update response.
|
||
|
|
$transient->response[ $this->plugin_basename ] = (object) array(
|
||
|
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||
|
|
'plugin' => $this->plugin_basename,
|
||
|
|
'new_version' => $update_info->version,
|
||
|
|
'url' => $update_info->homepage ?? '',
|
||
|
|
'package' => $update_info->downloadUrl,
|
||
|
|
'icons' => $update_info->icons ?? array(),
|
||
|
|
'tested' => $update_info->tested ?? '',
|
||
|
|
'requires' => $update_info->requires ?? '',
|
||
|
|
'requires_php' => $update_info->requiresPhp ?? '',
|
||
|
|
);
|
||
|
|
|
||
|
|
return $transient;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Provide plugin information for the details modal.
|
||
|
|
*
|
||
|
|
* @param false|object|array $result The result object or array.
|
||
|
|
* @param string $action The API action being performed.
|
||
|
|
* @param object $args Plugin API arguments.
|
||
|
|
* @return false|object
|
||
|
|
*/
|
||
|
|
public function plugin_info( $result, string $action, object $args ) {
|
||
|
|
if ( 'plugin_information' !== $action ) {
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! isset( $args->slug ) || $args->slug !== $this->plugin_slug ) {
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$update_info = $this->get_update_info();
|
||
|
|
|
||
|
|
if ( null === $update_info ) {
|
||
|
|
return $result;
|
||
|
|
}
|
||
|
|
|
||
|
|
$plugin_info = (object) array(
|
||
|
|
'name' => $update_info->name ?? 'WP BnB Manager',
|
||
|
|
'slug' => $update_info->slug ?? $this->plugin_slug,
|
||
|
|
'version' => $update_info->version ?? $this->current_version,
|
||
|
|
'author' => '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>',
|
||
|
|
'homepage' => $update_info->homepage ?? 'https://src.bundespruefstelle.ch/magdev/wp-bnb',
|
||
|
|
'requires' => $update_info->requires ?? '6.0',
|
||
|
|
'tested' => $update_info->tested ?? '',
|
||
|
|
'requires_php' => $update_info->requiresPhp ?? '8.3',
|
||
|
|
'last_updated' => $update_info->lastUpdated?->format( 'Y-m-d' ) ?? '',
|
||
|
|
'download_link' => $update_info->downloadUrl ?? '',
|
||
|
|
'sections' => $update_info->sections ?? array(
|
||
|
|
'description' => __( 'A comprehensive Bed & Breakfast management plugin for WordPress.', 'wp-bnb' ),
|
||
|
|
'changelog' => $update_info->changelog ?? '',
|
||
|
|
),
|
||
|
|
);
|
||
|
|
|
||
|
|
if ( ! empty( $update_info->icons ) ) {
|
||
|
|
$plugin_info->icons = $update_info->icons;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $plugin_info;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear update cache after upgrade.
|
||
|
|
*
|
||
|
|
* @param \WP_Upgrader $upgrader WP_Upgrader instance.
|
||
|
|
* @param array $hook_extra Extra arguments passed to hooked filters.
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function after_update( \WP_Upgrader $upgrader, array $hook_extra ): void {
|
||
|
|
if ( ! isset( $hook_extra['plugins'] ) || ! is_array( $hook_extra['plugins'] ) ) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( in_array( $this->plugin_basename, $hook_extra['plugins'], true ) ) {
|
||
|
|
$this->clear_cache();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get update info from cache or server.
|
||
|
|
*
|
||
|
|
* @param bool $force_refresh Force refresh from server.
|
||
|
|
* @return UpdateInfo|null
|
||
|
|
*/
|
||
|
|
public function get_cached_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||
|
|
if ( ! $force_refresh ) {
|
||
|
|
$cached = get_transient( self::CACHE_KEY );
|
||
|
|
if ( false !== $cached && $cached instanceof UpdateInfo ) {
|
||
|
|
return $cached;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if license is configured.
|
||
|
|
$license_key = Manager::get_license_key();
|
||
|
|
if ( empty( $license_key ) ) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( ! $this->init_client() ) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
$domain = $this->get_current_domain();
|
||
|
|
$update_info = $this->client->checkForUpdates(
|
||
|
|
licenseKey: $license_key,
|
||
|
|
domain: $domain,
|
||
|
|
pluginSlug: $this->plugin_slug,
|
||
|
|
currentVersion: $this->current_version,
|
||
|
|
);
|
||
|
|
|
||
|
|
// Cache the result and update last check timestamp.
|
||
|
|
set_transient( self::CACHE_KEY, $update_info, $this->get_cache_duration() );
|
||
|
|
update_option( self::LAST_CHECK_KEY, time() );
|
||
|
|
|
||
|
|
return $update_info;
|
||
|
|
} catch ( \Throwable $e ) {
|
||
|
|
// Silently fail and return null - don't break WordPress.
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get update info from cache or server (alias for WordPress update system).
|
||
|
|
*
|
||
|
|
* @param bool $force_refresh Force refresh from server.
|
||
|
|
* @return UpdateInfo|null
|
||
|
|
*/
|
||
|
|
private function get_update_info( bool $force_refresh = false ): ?UpdateInfo {
|
||
|
|
return $this->get_cached_update_info( $force_refresh );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get current domain.
|
||
|
|
*
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
private function get_current_domain(): string {
|
||
|
|
$site_url = get_site_url();
|
||
|
|
$parsed = wp_parse_url( $site_url );
|
||
|
|
return $parsed['host'] ?? '';
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Clear the update cache.
|
||
|
|
*
|
||
|
|
* @return void
|
||
|
|
*/
|
||
|
|
public function clear_cache(): void {
|
||
|
|
delete_transient( self::CACHE_KEY );
|
||
|
|
}
|
||
|
|
}
|