pluginSlug = 'wc-licensed-product'; $this->pluginBasename = WC_LICENSED_PRODUCT_PLUGIN_BASENAME; } /** * Register WordPress hooks for update checking */ public function register(): void { // Skip if update notifications are disabled if ($this->isUpdateNotificationDisabled()) { return; } // Check for updates add_filter('pre_set_site_transient_update_plugins', [$this, 'checkForUpdates']); // Provide plugin information for the update modal add_filter('plugins_api', [$this, 'getPluginInfo'], 10, 3); // Add authentication headers to download requests add_filter('http_request_args', [$this, 'addAuthHeaders'], 10, 2); // Handle auto-install setting add_filter('auto_update_plugin', [$this, 'handleAutoInstall'], 10, 2); // Clear cache on settings save add_action('update_option_wc_licensed_product_plugin_license_key', [$this, 'clearCache']); add_action('update_option_wc_licensed_product_plugin_license_server_url', [$this, 'clearCache']); add_action('update_option_wc_licensed_product_update_notification_enabled', [$this, 'clearCache']); } /** * Check if update notifications are disabled */ private function isUpdateNotificationDisabled(): bool { // Check constant if (defined('WC_LICENSE_DISABLE_AUTO_UPDATE') && WC_LICENSE_DISABLE_AUTO_UPDATE) { return true; } // Check setting return !SettingsController::isUpdateNotificationEnabled(); } /** * Handle auto-install setting for WordPress automatic updates * * @param bool|null $update The update decision * @param object $item The plugin update object * @return bool|null Whether to auto-update this plugin */ public function handleAutoInstall($update, $item): ?bool { // Only handle our plugin if (!isset($item->plugin) || $item->plugin !== $this->pluginBasename) { return $update; } // Return true to enable auto-install, false to disable, or null to use default return SettingsController::isAutoInstallEnabled() ? true : $update; } /** * Check for plugin updates * * @param object $transient The update_plugins transient * @return object Modified transient */ public function checkForUpdates($transient) { if (empty($transient->checked)) { return $transient; } // Get cached update info or fetch fresh $updateInfo = $this->getUpdateInfo(); if (!$updateInfo || !isset($updateInfo['update_available']) || !$updateInfo['update_available']) { return $transient; } // Compare versions $currentVersion = $transient->checked[$this->pluginBasename] ?? WC_LICENSED_PRODUCT_VERSION; if (version_compare($updateInfo['version'], $currentVersion, '>')) { $transient->response[$this->pluginBasename] = $this->buildUpdateObject($updateInfo); } return $transient; } /** * Get plugin information for the update modal * * @param false|object|array $result The result object or array * @param string $action The API action * @param object $args Request arguments * @return false|object */ public function getPluginInfo($result, string $action, object $args) { if ($action !== 'plugin_information') { return $result; } if (!isset($args->slug) || $args->slug !== $this->pluginSlug) { return $result; } // Get update info $updateInfo = $this->getUpdateInfo(true); if (!$updateInfo) { return $result; } return $this->buildPluginInfoObject($updateInfo); } /** * Add authentication headers to download requests * * @param array $args HTTP request arguments * @param string $url Request URL * @return array Modified arguments */ public function addAuthHeaders(array $args, string $url): array { // Only modify requests to our license server $serverUrl = $this->getLicenseServerUrl(); if (empty($serverUrl) || strpos($url, parse_url($serverUrl, PHP_URL_HOST)) === false) { return $args; } // Only modify download requests if (strpos($url, 'license-download') === false) { return $args; } // Add license key to headers for potential server-side verification $licenseKey = $this->getLicenseKey(); if (!empty($licenseKey)) { $args['headers']['X-License-Key'] = $licenseKey; } return $args; } /** * Get update info from cache or server * * @param bool $forceRefresh Force refresh from server * @return array|null Update info or null if unavailable */ public function getUpdateInfo(bool $forceRefresh = false): ?array { // Check cache unless force refresh if (!$forceRefresh) { $cached = get_transient(self::CACHE_KEY); if ($cached !== false) { return $cached; } } // Fetch from server $updateInfo = $this->fetchUpdateInfo(); if ($updateInfo) { // Cache the result $cacheTtl = $this->getCacheTtl(); set_transient(self::CACHE_KEY, $updateInfo, $cacheTtl); } return $updateInfo; } /** * Fetch update info from the license server */ private function fetchUpdateInfo(): ?array { $serverUrl = $this->getLicenseServerUrl(); $licenseKey = $this->getLicenseKey(); if (empty($serverUrl) || empty($licenseKey)) { return null; } try { $httpClient = HttpClient::create([ 'timeout' => 15, 'verify_peer' => true, ]); $updateCheckUrl = rtrim($serverUrl, '/') . '/wp-json/wc-licensed-product/v1/update-check'; $response = $httpClient->request('POST', $updateCheckUrl, [ 'json' => [ 'license_key' => $licenseKey, 'domain' => $this->getCurrentDomain(), 'plugin_slug' => $this->pluginSlug, 'current_version' => WC_LICENSED_PRODUCT_VERSION, ], ]); if ($response->getStatusCode() !== 200) { return null; } $data = $response->toArray(); // Verify response structure if (!isset($data['success']) || !$data['success']) { return null; } return $data; } catch (\Throwable $e) { // Log error but don't break the site if (defined('WP_DEBUG') && WP_DEBUG) { error_log('WC Licensed Product: Update check failed - ' . $e->getMessage()); } return null; } } /** * Build WordPress update object for transient */ private function buildUpdateObject(array $updateInfo): object { $update = new \stdClass(); $update->id = $this->pluginSlug; $update->slug = $updateInfo['slug'] ?? $this->pluginSlug; $update->plugin = $this->pluginBasename; $update->new_version = $updateInfo['version']; $update->url = $updateInfo['homepage'] ?? ''; $update->package = $updateInfo['download_url'] ?? $updateInfo['package'] ?? ''; if (isset($updateInfo['tested'])) { $update->tested = $updateInfo['tested']; } if (isset($updateInfo['requires'])) { $update->requires = $updateInfo['requires']; } if (isset($updateInfo['requires_php'])) { $update->requires_php = $updateInfo['requires_php']; } if (isset($updateInfo['icons'])) { $update->icons = $updateInfo['icons']; } return $update; } /** * Build plugin info object for plugins_api */ private function buildPluginInfoObject(array $updateInfo): object { $info = new \stdClass(); $info->name = $updateInfo['name'] ?? 'WC Licensed Product'; $info->slug = $updateInfo['slug'] ?? $this->pluginSlug; $info->version = $updateInfo['version']; $info->author = 'Marco Graetsch'; $info->homepage = $updateInfo['homepage'] ?? ''; $info->requires = $updateInfo['requires'] ?? '6.0'; $info->tested = $updateInfo['tested'] ?? ''; $info->requires_php = $updateInfo['requires_php'] ?? '8.3'; $info->downloaded = 0; $info->last_updated = $updateInfo['last_updated'] ?? ''; $info->download_link = $updateInfo['download_url'] ?? $updateInfo['package'] ?? ''; // Sections for the modal $info->sections = []; if (isset($updateInfo['sections']['description'])) { $info->sections['description'] = $updateInfo['sections']['description']; } else { $info->sections['description'] = __( 'WooCommerce plugin for selling licensed software products with domain-bound license keys.', 'wc-licensed-product' ); } if (isset($updateInfo['sections']['changelog']) || isset($updateInfo['changelog'])) { $info->sections['changelog'] = $updateInfo['sections']['changelog'] ?? $updateInfo['changelog']; } // Banners and icons if (isset($updateInfo['banners'])) { $info->banners = $updateInfo['banners']; } if (isset($updateInfo['icons'])) { $info->icons = $updateInfo['icons']; } return $info; } /** * Clear the update cache */ public function clearCache(): void { delete_transient(self::CACHE_KEY); } /** * Get cache TTL from settings or default */ private function getCacheTtl(): int { $hours = (int) get_option('wc_licensed_product_update_check_frequency', 12); return max(1, $hours) * HOUR_IN_SECONDS; } /** * Get the license server URL from settings */ private function getLicenseServerUrl(): string { // Check constant override first if (defined('WC_LICENSE_UPDATE_CHECK_URL') && WC_LICENSE_UPDATE_CHECK_URL) { return WC_LICENSE_UPDATE_CHECK_URL; } return (string) get_option('wc_licensed_product_plugin_license_server_url', ''); } /** * Get the license key from settings */ private function getLicenseKey(): string { return (string) get_option('wc_licensed_product_plugin_license_key', ''); } /** * Get the current domain from the site URL */ private function getCurrentDomain(): string { $siteUrl = get_site_url(); $parsed = parse_url($siteUrl); $host = $parsed['host'] ?? 'localhost'; if (isset($parsed['port'])) { $host .= ':' . $parsed['port']; } return strtolower($host); } /** * Force an immediate update check * * Useful for admin interfaces where user clicks "Check for updates" */ public function forceUpdateCheck(): ?array { $this->clearCache(); return $this->getUpdateInfo(true); } }