response[ $basename ] ) ) { unset( $transient->response[ $basename ] ); } return $transient; } public static function on_activation(): void { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', self::CRON_HOOK ); } } public static function on_deactivation(): void { $ts = wp_next_scheduled( self::CRON_HOOK ); if ( $ts ) { wp_unschedule_event( $ts, self::CRON_HOOK ); } } /* ───────────────────────── helpers ───────────────────────── */ public static function get_license(): array { $saved = get_option( CB_LICENSE_OPTION, [] ); $lic = wp_parse_args( is_array( $saved ) ? $saved : [], [ 'key' => '', 'status' => 'inactive', // inactive | active | invalid | limit 'domain' => '', 'last_check' => 0, 'message' => '', 'pending_domains' => [], ] ); if ( ! is_array( $lic['pending_domains'] ) ) { $lic['pending_domains'] = []; } return $lic; } public static function is_active(): bool { return self::get_license()['status'] === 'active'; } /** Mask a key for display: keep dashes + last 4 chars, hide the rest. */ public static function mask_key( string $key ): string { $n = strlen( $key ); if ( $n <= 4 ) { return $key; } $tail = substr( $key, -4 ); $head = preg_replace( '/[A-Za-z0-9]/', '•', substr( $key, 0, $n - 4 ) ); return $head . $tail; } public static function api_url(): string { return untrailingslashit( (string) apply_filters( 'cb_license_api_url', CB_LICENSE_API_URL ) ); } public static function domain(): string { $host = (string) wp_parse_url( home_url(), PHP_URL_HOST ); $host = strtolower( $host ); return (string) preg_replace( '/^www\./', '', $host ); } /** POST JSON to a backend endpoint. Returns decoded array or WP_Error. */ private static function remote( string $endpoint, array $body ): array|WP_Error { $response = wp_remote_post( self::api_url() . $endpoint, [ 'timeout' => 15, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'body' => wp_json_encode( $body ), ] ); if ( is_wp_error( $response ) ) { return $response; } $code = wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $data ) ) { return new WP_Error( 'cb_bad_response', __( 'Ungültige Antwort vom Lizenzserver.', 'gdpr-content-blocker' ) ); } $data['_http_code'] = $code; return $data; } /* ───────────────────────── actions ───────────────────────── */ public static function handle_activate(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ); } check_admin_referer( 'cb_license', 'cb_license_nonce' ); $key = isset( $_POST['cb_license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['cb_license_key'] ) ) : ''; $key = trim( $key ); if ( $key === '' ) { self::store( '', 'inactive', __( 'Bitte einen Lizenzschlüssel eingeben.', 'gdpr-content-blocker' ) ); self::redirect(); } $result = self::remote( '/api/v1/activate', [ 'key' => $key, 'product' => CB_PRODUCT_SLUG, 'domain' => self::domain(), ] ); if ( is_wp_error( $result ) ) { self::store( $key, 'inactive', $result->get_error_message() ); self::redirect(); } if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) { self::store( $key, 'active', __( 'Lizenz erfolgreich aktiviert.', 'gdpr-content-blocker' ) ); } elseif ( ( $result['code'] ?? '' ) === 'limit_reached' ) { // No free slots: keep the key and offer to release one of the bound domains. $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : []; self::store( $key, 'limit', __( 'Alle Plätze dieser Lizenz sind belegt. Geben Sie eine Domain frei, um diese Seite zu aktivieren.', 'gdpr-content-blocker' ), $domains ); } else { $msg = $result['error'] ?? __( 'Aktivierung fehlgeschlagen.', 'gdpr-content-blocker' ); self::store( $key, 'invalid', $msg ); } self::redirect(); } /** * Free a chosen domain's slot on the backend, then activate the current site. * Triggered from the "no free slots" selection UI. */ public static function handle_swap(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ); } check_admin_referer( 'cb_license', 'cb_license_nonce' ); $lic = self::get_license(); $key = $lic['key']; if ( $key === '' ) { self::redirect(); } $release = isset( $_POST['cb_release_domain'] ) ? sanitize_text_field( wp_unslash( $_POST['cb_release_domain'] ) ) : ''; // Only allow releasing a domain that was actually reported as occupied. if ( $release === '' || ! in_array( $release, $lic['pending_domains'], true ) ) { self::store( $key, 'limit', __( 'Bitte eine gültige Domain zum Freigeben auswählen.', 'gdpr-content-blocker' ), $lic['pending_domains'] ); self::redirect(); } // 1) Release the chosen domain. $deact = self::remote( '/api/v1/deactivate', [ 'key' => $key, 'product' => CB_PRODUCT_SLUG, 'domain' => $release, ] ); if ( is_wp_error( $deact ) ) { self::store( $key, 'limit', $deact->get_error_message(), $lic['pending_domains'] ); self::redirect(); } // 2) Activate this site. $result = self::remote( '/api/v1/activate', [ 'key' => $key, 'product' => CB_PRODUCT_SLUG, 'domain' => self::domain(), ] ); if ( is_wp_error( $result ) ) { self::store( $key, 'limit', $result->get_error_message(), $lic['pending_domains'] ); } elseif ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) { self::store( $key, 'active', sprintf( /* translators: %s: released domain */ __( 'Domain %s freigegeben und diese Seite aktiviert.', 'gdpr-content-blocker' ), $release ) ); } else { $domains = is_array( $result['domains'] ?? null ) ? $result['domains'] : $lic['pending_domains']; $msg = $result['error'] ?? __( 'Aktivierung fehlgeschlagen.', 'gdpr-content-blocker' ); self::store( $key, 'limit', $msg, $domains ); } self::redirect(); } public static function handle_deactivate(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Keine Berechtigung.', 'gdpr-content-blocker' ) ); } check_admin_referer( 'cb_license', 'cb_license_nonce' ); $lic = self::get_license(); if ( $lic['key'] !== '' ) { // Free the activation slot on the backend (best effort). self::remote( '/api/v1/deactivate', [ 'key' => $lic['key'], 'product' => CB_PRODUCT_SLUG, 'domain' => self::domain(), ] ); } delete_option( CB_LICENSE_OPTION ); CB_Updater::flush_cache(); delete_site_transient( 'update_plugins' ); self::redirect(); } public static function cron_check(): void { $lic = self::get_license(); // Only re-validate licenses that are bound to THIS domain. In the // 'inactive' and 'limit' states this site isn't activated, so /validate // would always 403 and wrongly overwrite the (recoverable) state. if ( $lic['key'] === '' || ! in_array( $lic['status'], [ 'active', 'invalid' ], true ) ) { return; } $result = self::remote( '/api/v1/validate', [ 'key' => $lic['key'], 'product' => CB_PRODUCT_SLUG, 'domain' => self::domain(), ] ); // On network error: keep the previous status (grace), just record the time. if ( is_wp_error( $result ) ) { self::store( $lic['key'], $lic['status'], $lic['message'] ); return; } if ( ! empty( $result['ok'] ) && ( $result['status'] ?? '' ) === 'valid' ) { self::store( $lic['key'], 'active', '' ); } else { $msg = $result['error'] ?? __( 'Lizenz nicht mehr gültig.', 'gdpr-content-blocker' ); self::store( $lic['key'], 'invalid', $msg ); } } private static function store( string $key, string $status, string $message, array $pending_domains = [] ): void { update_option( CB_LICENSE_OPTION, [ 'key' => $key, 'status' => $status, 'domain' => self::domain(), 'last_check' => time(), 'message' => $message, 'pending_domains' => array_values( array_filter( array_map( 'strval', $pending_domains ) ) ), ] ); // License state changed → invalidate cached update info and force WP to // re-evaluate available updates (so the gate takes effect immediately). if ( class_exists( 'CB_Updater' ) ) { CB_Updater::flush_cache(); } delete_site_transient( 'update_plugins' ); } private static function redirect(): void { wp_safe_redirect( admin_url( 'options-general.php?page=gdpr-content-blocker&cb_tab=license' ) ); exit; } /* ───────────────────────── UI ───────────────────────── */ public static function maybe_notice(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } $screen = get_current_screen(); if ( $screen && $screen->id === 'settings_page_gdpr-content-blocker' ) { return; // The tab already shows the status; don't double up. } if ( self::is_active() ) { return; } $url = admin_url( 'options-general.php?page=gdpr-content-blocker&cb_tab=license' ); echo '

'; printf( /* translators: %s: link to the license settings */ esc_html__( 'GDPR Content Blocker ist nicht lizenziert. Der Schutz bleibt aktiv, aber für Updates und Support bitte %s hinterlegen.', 'gdpr-content-blocker' ), '' . esc_html__( 'Lizenzschlüssel', 'gdpr-content-blocker' ) . '' ); echo '

'; } public static function render_tab(): void { $lic = self::get_license(); $status = $lic['status']; $badge = match ( $status ) { 'active' => '● ' . esc_html__( 'Aktiv', 'gdpr-content-blocker' ) . '', 'invalid' => '● ' . esc_html__( 'Ungültig', 'gdpr-content-blocker' ) . '', 'limit' => '● ' . esc_html__( 'Alle Plätze belegt', 'gdpr-content-blocker' ) . '', default => '● ' . esc_html__( 'Nicht aktiviert', 'gdpr-content-blocker' ) . '', }; ?>