You've already forked wc-tier-and-package-prices
469 lines
15 KiB
PHP
469 lines
15 KiB
PHP
|
|
<?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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|