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'] ?? 'Marco Graetsch'; $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(); } } }