init_hooks(); } /** * Initialize WordPress hooks. * * @return void */ private function init_hooks(): void { // AJAX handlers. add_action( 'wp_ajax_wp_bnb_validate_license', array( $this, 'ajax_validate_license' ) ); add_action( 'wp_ajax_wp_bnb_activate_license', array( $this, 'ajax_activate_license' ) ); add_action( 'wp_ajax_wp_bnb_check_status', array( $this, 'ajax_check_status' ) ); } /** * Initialize the license client. * * @return bool */ private function init_client(): bool { if ( null !== $this->client ) { return true; } $server_url = self::get_server_url(); $server_secret = self::get_server_secret(); if ( empty( $server_url ) ) { return false; } try { // Use SecureLicenseClient if server secret is configured. 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 if license is valid. * * Localhost environments bypass the license check to allow * full functionality during development. * * @return bool */ public static function is_license_valid(): bool { // Bypass license check for localhost environments. if ( self::is_localhost() ) { return true; } $status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); return 'valid' === $status; } /** * Check if running on localhost. * * Detects common local development environments: * - localhost / 127.0.0.1 / ::1 * - .local, .test, .localhost domains * - Private IP ranges (192.168.x.x, 10.x.x.x, 172.16-31.x.x) * * @return bool */ public static function is_localhost(): bool { $site_url = get_site_url(); $parsed = wp_parse_url( $site_url ); $host = $parsed['host'] ?? ''; // Check for localhost variations. if ( in_array( $host, array( 'localhost', '127.0.0.1', '::1' ), true ) ) { return true; } // Check for common local development TLDs. $local_tlds = array( '.local', '.test', '.localhost', '.dev', '.ddev.site' ); foreach ( $local_tlds as $tld ) { if ( str_ends_with( $host, $tld ) ) { return true; } } // Check for private IP ranges. if ( filter_var( $host, FILTER_VALIDATE_IP ) ) { // 10.x.x.x, 172.16-31.x.x, 192.168.x.x. if ( ! filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE ) ) { return true; } } return false; } /** * Get license key. * * @return string */ public static function get_license_key(): string { return get_option( self::OPTION_LICENSE_KEY, '' ); } /** * Get server URL. * * @return string */ public static function get_server_url(): string { return get_option( self::OPTION_SERVER_URL, '' ); } /** * Get server secret. * * @return string */ public static function get_server_secret(): string { return get_option( self::OPTION_SERVER_SECRET, '' ); } /** * Get cached license status. * * @return string */ public static function get_cached_status(): string { $server_url = self::get_server_url(); $license_key = self::get_license_key(); if ( empty( $server_url ) || empty( $license_key ) ) { return 'unconfigured'; } return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); } /** * Get cached license data. * * @return array */ public static function get_cached_data(): array { return get_option( self::OPTION_LICENSE_DATA, array() ); } /** * Get last check timestamp. * * @return int */ public static function get_last_check(): int { return (int) get_option( self::OPTION_LAST_CHECK, 0 ); } /** * Save license settings. * * @param array $data Settings data. * @return bool */ public static function save_settings( array $data ): bool { if ( isset( $data['license_key'] ) ) { update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) ); } if ( isset( $data['server_url'] ) ) { update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) ); } // Only update secret if provided (not empty). if ( ! empty( $data['server_secret'] ) ) { update_option( self::OPTION_SERVER_SECRET, sanitize_text_field( $data['server_secret'] ) ); } // Clear cached status when settings change. delete_transient( self::TRANSIENT_LICENSE_CHECK ); return true; } /** * Clear all license data. * * @return void */ public static function clear_license_data(): void { update_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); update_option( self::OPTION_LICENSE_DATA, array() ); update_option( self::OPTION_LAST_CHECK, 0 ); delete_transient( self::TRANSIENT_LICENSE_CHECK ); } /** * Validate license. * * @return array */ public function validate(): array { if ( ! $this->init_client() ) { return array( 'success' => false, 'message' => __( 'License client could not be initialized. Please check server URL and secret.', 'wp-bnb' ), 'status' => 'unconfigured', ); } $license_key = self::get_license_key(); if ( empty( $license_key ) ) { return array( 'success' => false, 'message' => __( 'Please enter a license key.', 'wp-bnb' ), 'status' => 'unconfigured', ); } $domain = $this->get_current_domain(); try { $license_info = $this->client->validate( $license_key, $domain ); // Store license data. $license_data = array( 'product_id' => $license_info->productId, 'expires' => $license_info->expiresAt?->format( 'Y-m-d H:i:s' ), ); update_option( self::OPTION_LICENSE_STATUS, 'valid' ); update_option( self::OPTION_LICENSE_DATA, $license_data ); update_option( self::OPTION_LAST_CHECK, time() ); set_transient( self::TRANSIENT_LICENSE_CHECK, 'valid', self::CACHE_TTL ); return array( 'success' => true, 'message' => __( 'License is valid.', 'wp-bnb' ), 'status' => 'valid', 'data' => $license_data, ); } catch ( LicenseNotFoundException $e ) { return $this->handle_license_error( 'invalid', __( 'License key not found.', 'wp-bnb' ) ); } catch ( LicenseExpiredException $e ) { return $this->handle_license_error( 'expired', __( 'License has expired.', 'wp-bnb' ) ); } catch ( LicenseRevokedException $e ) { return $this->handle_license_error( 'revoked', __( 'License has been revoked.', 'wp-bnb' ) ); } catch ( LicenseInactiveException $e ) { return $this->handle_license_error( 'inactive', __( 'License is inactive. Please activate it first.', 'wp-bnb' ) ); } catch ( DomainMismatchException $e ) { return $this->handle_license_error( 'invalid', __( 'License is not valid for this domain.', 'wp-bnb' ) ); } catch ( SignatureException $e ) { return $this->handle_license_error( 'invalid', __( 'Response signature verification failed. Please check server secret.', 'wp-bnb' ) ); } catch ( RateLimitExceededException $e ) { return array( 'success' => false, 'message' => sprintf( /* translators: %d: Seconds to wait */ __( 'Rate limit exceeded. Please try again in %d seconds.', 'wp-bnb' ), $e->retryAfter ), 'status' => self::get_cached_status(), ); } catch ( \Throwable $e ) { return $this->handle_license_error( 'invalid', __( 'An error occurred while validating the license.', 'wp-bnb' ) ); } } /** * Activate license. * * @return array */ public function activate(): array { if ( ! $this->init_client() ) { return array( 'success' => false, 'message' => __( 'License client could not be initialized. Please check server URL and secret.', 'wp-bnb' ), 'status' => 'unconfigured', ); } $license_key = self::get_license_key(); if ( empty( $license_key ) ) { return array( 'success' => false, 'message' => __( 'Please enter a license key.', 'wp-bnb' ), 'status' => 'unconfigured', ); } $domain = $this->get_current_domain(); try { $result = $this->client->activate( $license_key, $domain ); if ( $result->success ) { // Validate after activation to get full license info. return $this->validate(); } return array( 'success' => false, 'message' => __( 'Activation failed.', 'wp-bnb' ), 'status' => 'inactive', ); } catch ( MaxActivationsReachedException $e ) { return $this->handle_license_error( 'invalid', __( 'Maximum activations reached for this license.', 'wp-bnb' ) ); } catch ( LicenseNotFoundException $e ) { return $this->handle_license_error( 'invalid', __( 'License key not found.', 'wp-bnb' ) ); } catch ( LicenseExpiredException $e ) { return $this->handle_license_error( 'expired', __( 'License has expired.', 'wp-bnb' ) ); } catch ( SignatureException $e ) { return $this->handle_license_error( 'invalid', __( 'Response signature verification failed. Please check server secret.', 'wp-bnb' ) ); } catch ( RateLimitExceededException $e ) { return array( 'success' => false, 'message' => sprintf( /* translators: %d: Seconds to wait */ __( 'Rate limit exceeded. Please try again in %d seconds.', 'wp-bnb' ), $e->retryAfter ), 'status' => self::get_cached_status(), ); } catch ( \Throwable $e ) { return $this->handle_license_error( 'invalid', __( 'An error occurred while activating the license.', 'wp-bnb' ) ); } } /** * Get license status. * * @param bool $force_refresh Whether to force a refresh from server. * @return array */ public function get_status( bool $force_refresh = false ): array { // Check transient cache first. if ( ! $force_refresh ) { $cached = get_transient( self::TRANSIENT_LICENSE_CHECK ); if ( false !== $cached ) { return array( 'success' => 'valid' === $cached, 'status' => $cached, 'data' => self::get_cached_data(), 'cached' => true, ); } } if ( ! $this->init_client() ) { return array( 'success' => false, 'status' => 'unconfigured', 'message' => __( 'License client not configured.', 'wp-bnb' ), ); } $license_key = self::get_license_key(); if ( empty( $license_key ) ) { return array( 'success' => false, 'status' => 'unconfigured', 'message' => __( 'No license key configured.', 'wp-bnb' ), ); } try { $status_info = $this->client->status( $license_key ); $status = match ( $status_info->status ) { LicenseState::Active => 'valid', LicenseState::Inactive => 'inactive', LicenseState::Expired => 'expired', LicenseState::Revoked => 'revoked', }; update_option( self::OPTION_LICENSE_STATUS, $status ); update_option( self::OPTION_LAST_CHECK, time() ); set_transient( self::TRANSIENT_LICENSE_CHECK, $status, self::CACHE_TTL ); return array( 'success' => 'valid' === $status, 'status' => $status, 'data' => self::get_cached_data(), 'cached' => false, ); } catch ( \Throwable $e ) { return array( 'success' => false, 'status' => 'invalid', 'message' => __( 'Failed to check license status.', 'wp-bnb' ), ); } } /** * Handle license error. * * @param string $status Status code. * @param string $message Error message. * @return array */ private function handle_license_error( string $status, string $message ): array { update_option( self::OPTION_LICENSE_STATUS, $status ); update_option( self::OPTION_LAST_CHECK, time() ); delete_transient( self::TRANSIENT_LICENSE_CHECK ); return array( 'success' => false, 'message' => $message, 'status' => $status, ); } /** * 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'] ?? ''; } /** * AJAX handler: Validate license. * * @return void */ public function ajax_validate_license(): void { check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ), ) ); } $result = $this->validate(); if ( $result['success'] ) { wp_send_json_success( $result ); } else { wp_send_json_error( $result ); } } /** * AJAX handler: Activate license. * * @return void */ public function ajax_activate_license(): void { check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ), ) ); } $result = $this->activate(); if ( $result['success'] ) { wp_send_json_success( $result ); } else { wp_send_json_error( $result ); } } /** * AJAX handler: Check status. * * @return void */ public function ajax_check_status(): void { check_ajax_referer( 'wp_bnb_admin_nonce', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => __( 'You do not have permission to perform this action.', 'wp-bnb' ), ) ); } $force_refresh = isset( $_POST['force'] ) && 'true' === $_POST['force']; $result = $this->get_status( $force_refresh ); if ( $result['success'] ) { wp_send_json_success( $result ); } else { wp_send_json_error( $result ); } } }