Files
wc-tier-and-package-prices/includes/class-wc-tpp-update-checker.php

469 lines
15 KiB
PHP
Raw Permalink Normal View History

<?php
/**
* Update Checker
*
* Handles WordPress plugin updates from the license server
*
* @package WC_Tier_Package_Prices
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* WC_TPP_Update_Checker class
*/
if (!class_exists('WC_TPP_Update_Checker')) {
class WC_TPP_Update_Checker {
/**
* Singleton instance
*
* @var WC_TPP_Update_Checker|null
*/
private static $instance = null;
/**
* Plugin slug
*/
private const PLUGIN_SLUG = 'wc-tier-and-package-prices';
/**
* Update check cache key
*/
private const CACHE_KEY = 'wc_tpp_update_info';
/**
* Default check frequency in hours
*/
private const DEFAULT_CHECK_FREQUENCY = 12;
/**
* Get singleton instance
*
* @return WC_TPP_Update_Checker
*/
public static function get_instance(): WC_TPP_Update_Checker {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Only register if updates are not disabled
if ($this->is_disabled()) {
return;
}
$this->register_hooks();
}
/**
* Check if auto-updates are disabled via constant
*
* @return bool
*/
private function is_disabled(): bool {
return defined('WC_TPP_DISABLE_AUTO_UPDATE') && WC_TPP_DISABLE_AUTO_UPDATE;
}
/**
* Register WordPress hooks
*/
private function register_hooks(): void {
// Check for updates
add_filter('pre_set_site_transient_update_plugins', array($this, 'check_for_updates'));
// Provide plugin information for update modal
add_filter('plugins_api', array($this, 'get_plugin_info'), 10, 3);
// Add authentication headers to download requests
add_filter('http_request_args', array($this, 'add_auth_headers'), 10, 2);
// Handle auto-install setting
add_filter('auto_update_plugin', array($this, 'handle_auto_install'), 10, 2);
// Clear cache when settings change
add_action('update_option_wc_tpp_license_key', array($this, 'clear_cache'));
add_action('update_option_wc_tpp_license_server_url', array($this, 'clear_cache'));
add_action('update_option_wc_tpp_update_notification_enabled', array($this, 'clear_cache'));
}
/**
* Check if update notifications are enabled
*
* @return bool
*/
public static function is_update_notification_enabled(): bool {
return get_option('wc_tpp_update_notification_enabled', 'yes') === 'yes';
}
/**
* Check if auto-install is enabled
*
* @return bool
*/
public static function is_auto_install_enabled(): bool {
return get_option('wc_tpp_auto_install_enabled', 'no') === 'yes';
}
/**
* Get update check frequency in hours
*
* @return int
*/
public static function get_check_frequency(): int {
$frequency = (int) get_option('wc_tpp_update_check_frequency', self::DEFAULT_CHECK_FREQUENCY);
return max(1, min(168, $frequency)); // Clamp between 1 and 168 hours
}
/**
* Check for plugin updates
*
* @param object $transient WordPress update transient.
* @return object
*/
public function check_for_updates($transient) {
if (empty($transient->checked)) {
return $transient;
}
// Skip if notifications disabled
if (!self::is_update_notification_enabled()) {
return $transient;
}
// Get update info (cached)
$update_info = $this->get_update_info();
if (empty($update_info) || !$update_info['update_available']) {
return $transient;
}
// Build update object
$update_obj = $this->build_update_object($update_info);
if ($update_obj) {
$transient->response[WC_TPP_PLUGIN_BASENAME] = $update_obj;
}
return $transient;
}
/**
* Get plugin information for update modal
*
* @param false|object|array $result The result object or array.
* @param string $action The type of information being requested.
* @param object $args Plugin API arguments.
* @return false|object
*/
public function get_plugin_info($result, $action, $args) {
if ('plugin_information' !== $action) {
return $result;
}
if (!isset($args->slug) || self::PLUGIN_SLUG !== $args->slug) {
return $result;
}
// Get update info
$update_info = $this->get_update_info(true); // Force fetch for full info
if (empty($update_info)) {
return $result;
}
// Build plugin info object
return $this->build_plugin_info_object($update_info);
}
/**
* Add authentication headers to download requests
*
* @param array $args HTTP request arguments.
* @param string $url The request URL.
* @return array
*/
public function add_auth_headers(array $args, string $url): array {
$server_url = get_option('wc_tpp_license_server_url', '');
// Only add headers for requests to our license server
if (empty($server_url) || strpos($url, $server_url) !== 0) {
return $args;
}
$license_key = get_option('wc_tpp_license_key', '');
if (!empty($license_key)) {
$args['headers']['X-License-Key'] = $license_key;
}
return $args;
}
/**
* Handle auto-install setting
*
* @param bool|null $update Whether to auto-update.
* @param object $item The plugin update object.
* @return bool|null
*/
public function handle_auto_install($update, $item) {
// Check if this is our plugin
if (!isset($item->plugin) || WC_TPP_PLUGIN_BASENAME !== $item->plugin) {
return $update;
}
// Return our setting, or default behavior
if (self::is_auto_install_enabled()) {
return true;
}
return $update;
}
/**
* Get update info from cache or server
*
* @param bool $force_fetch Force fetching from server.
* @return array|null
*/
private function get_update_info(bool $force_fetch = false): ?array {
// Check cache first
if (!$force_fetch) {
$cached = get_transient(self::CACHE_KEY);
if (false !== $cached) {
return $cached;
}
}
// Fetch from server
$update_info = $this->fetch_update_info();
if ($update_info) {
// Cache the result
$cache_ttl = self::get_check_frequency() * HOUR_IN_SECONDS;
set_transient(self::CACHE_KEY, $update_info, $cache_ttl);
}
return $update_info;
}
/**
* Fetch update info from license server
*
* @return array|null
*/
private function fetch_update_info(): ?array {
$server_url = get_option('wc_tpp_license_server_url', '');
$license_key = get_option('wc_tpp_license_key', '');
$server_secret = get_option('wc_tpp_license_server_secret', '');
if (empty($server_url)) {
return null;
}
// Build the update check endpoint
$endpoint = trailingslashit($server_url) . 'wp-json/wc-licensed-product/v1/update-check';
// Get license checker for domain
$license_checker = WC_TPP_License_Checker::get_instance();
$domain = $license_checker->get_current_domain();
// Remove port for API call
$domain = preg_replace('/:[\d]+$/', '', $domain);
// Prepare request body
$body = array(
'license_key' => $license_key,
'domain' => $domain,
'plugin_slug' => self::PLUGIN_SLUG,
'current_version' => WC_TPP_VERSION,
);
// Make the request
$response = wp_remote_post($endpoint, array(
'timeout' => 15,
'headers' => array(
'Content-Type' => 'application/json',
'Accept' => 'application/json',
),
'body' => wp_json_encode($body),
));
if (is_wp_error($response)) {
return null;
}
$response_code = wp_remote_retrieve_response_code($response);
// Handle rate limiting
if (429 === $response_code) {
return null;
}
// Handle other errors
if ($response_code < 200 || $response_code >= 300) {
return null;
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (empty($data) || !isset($data['success'])) {
return null;
}
return $data;
}
/**
* Build WordPress update object
*
* @param array $update_info Update information from server.
* @return object|null
*/
private function build_update_object(array $update_info): ?object {
if (empty($update_info['version'])) {
return null;
}
// Check if update is actually available
if (version_compare(WC_TPP_VERSION, $update_info['version'], '>=')) {
return null;
}
$obj = new \stdClass();
$obj->id = $update_info['id'] ?? self::PLUGIN_SLUG;
$obj->slug = $update_info['slug'] ?? self::PLUGIN_SLUG;
$obj->plugin = $update_info['plugin'] ?? WC_TPP_PLUGIN_BASENAME;
$obj->new_version = $update_info['version'];
$obj->url = $update_info['url'] ?? '';
$obj->package = $update_info['download_url'] ?? $update_info['package'] ?? '';
$obj->tested = $update_info['tested'] ?? '';
$obj->requires = $update_info['requires'] ?? '6.0';
$obj->requires_php = $update_info['requires_php'] ?? '8.3';
// Icons
if (!empty($update_info['icons'])) {
$obj->icons = $update_info['icons'];
}
// Banners
if (!empty($update_info['banners'])) {
$obj->banners = $update_info['banners'];
}
return $obj;
}
/**
* Build plugin info object for update modal
*
* @param array $update_info Update information from server.
* @return object
*/
private function build_plugin_info_object(array $update_info): object {
$obj = new \stdClass();
$obj->name = $update_info['name'] ?? 'WooCommerce Tier and Package Prices';
$obj->slug = $update_info['slug'] ?? self::PLUGIN_SLUG;
$obj->version = $update_info['version'] ?? WC_TPP_VERSION;
$obj->author = $update_info['author'] ?? '<a href="https://src.bundespruefstelle.ch/magdev">Marco Graetsch</a>';
$obj->author_profile = $update_info['author_profile'] ?? 'https://src.bundespruefstelle.ch/magdev';
$obj->homepage = $update_info['homepage'] ?? $update_info['url'] ?? '';
$obj->requires = $update_info['requires'] ?? '6.0';
$obj->tested = $update_info['tested'] ?? '';
$obj->requires_php = $update_info['requires_php'] ?? '8.3';
$obj->last_updated = $update_info['last_updated'] ?? '';
$obj->download_link = $update_info['download_url'] ?? $update_info['package'] ?? '';
// Sections (description, changelog, etc.)
$obj->sections = array();
if (!empty($update_info['sections']['description'])) {
$obj->sections['description'] = $update_info['sections']['description'];
} else {
$obj->sections['description'] = __('Add tier pricing and package prices to WooCommerce products with configurable quantities at fixed prices.', 'wc-tier-package-prices');
}
if (!empty($update_info['sections']['changelog'])) {
$obj->sections['changelog'] = $update_info['sections']['changelog'];
} elseif (!empty($update_info['changelog'])) {
$obj->sections['changelog'] = $update_info['changelog'];
}
if (!empty($update_info['sections']['installation'])) {
$obj->sections['installation'] = $update_info['sections']['installation'];
}
// Icons
if (!empty($update_info['icons'])) {
$obj->icons = $update_info['icons'];
}
// Banners
if (!empty($update_info['banners'])) {
$obj->banners = $update_info['banners'];
}
return $obj;
}
/**
* Clear update cache
*/
public function clear_cache(): void {
delete_transient(self::CACHE_KEY);
// Also clear WordPress plugin update transient to force recheck
delete_site_transient('update_plugins');
}
/**
* Force check for updates
*
* @return array|null
*/
public function force_check(): ?array {
$this->clear_cache();
return $this->get_update_info(true);
}
/**
* Get the available update version if any
*
* @return string|null
*/
public function get_available_version(): ?string {
$update_info = $this->get_update_info();
if (empty($update_info) || empty($update_info['version'])) {
return null;
}
// Check if it's actually newer
if (version_compare(WC_TPP_VERSION, $update_info['version'], '>=')) {
return null;
}
return $update_info['version'];
}
/**
* Check if an update is available
*
* @return bool
*/
public function is_update_available(): bool {
return null !== $this->get_available_version();
}
}
}