16 ) { return false; } $method = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Compare the sanitized method with its original unslashed bytes only to reject rewriting; no raw value is stored, displayed or accepted as a method. return 'POST' === $method && $method === wp_unslash( $_SERVER['REQUEST_METHOD'] ); } public static function register(): void { add_action( 'admin_post_antivirus_ai_welcome', array( self::class, 'handle' ) ); add_action( 'admin_init', array( self::class, 'redirect' ) ); add_action( 'admin_init', array( self::class, 'schedule' ) ); add_action( self::HOOK, array( self::class, 'refresh' ) ); add_action( 'admin_enqueue_scripts', array( self::class, 'assets' ) ); } /** Called only by the activation hook; existing installs never get a new prompt. */ public static function activate( bool $existing ): void { $seen = get_option( 'antivirus_ai_welcome_seen', false ); add_option( 'antivirus_ai_welcome_seen', true, '', false ); if ( $existing || $seen || ( defined( 'WP_CLI' ) && WP_CLI ) || wp_doing_ajax() || is_network_admin() || ! current_user_can( 'manage_options' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- The original nonce is read only for type and byte-length guards; sanitize_text_field(wp_unslash()) runs here before the standalone verifier and any requested operation. $nonce = isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) && strlen( $_GET['_wpnonce'] ) <= 40 ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Original nonce bytes are used only to reject sanitizer changes; only the bounded sanitized nonce reaches the following standalone verifier. if ( '' === $nonce || strlen( $nonce ) > 20 || $nonce !== wp_unslash( $_GET['_wpnonce'] ) ) { return; } if ( ! wp_verify_nonce( $nonce, 'activate-plugin_' . ANTIVIRUS_AI_BASENAME ) ) { return; } $action = self::key( $_GET, 'action' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Only the original basename byte length is guarded; after core activation nonce verification, the value is unslashed, sanitized and compared with this exact plugin basename. $plugin = isset( $_GET['plugin'] ) && is_string( $_GET['plugin'] ) && strlen( $_GET['plugin'] ) <= 200 ? sanitize_text_field( wp_unslash( $_GET['plugin'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Original basename bytes are compared only to reject rewriting after sanitized exact-name and activation-nonce checks; they are never used as a path or destination. if ( 'activate' !== $action || ANTIVIRUS_AI_BASENAME !== $plugin || $plugin !== wp_unslash( $_GET['plugin'] ) ) { return; } set_transient( 'antivirus_ai_welcome_redirect_' . get_current_user_id(), true, 120 ); } public static function redirect(): void { global $pagenow; if ( ! current_user_can( 'manage_options' ) || wp_doing_ajax() || is_network_admin() || ( defined( 'WP_CLI' ) && WP_CLI ) ) { return; } $actor = get_current_user_id(); if ( 'plugins.php' === $pagenow ) { $key = 'antivirus_ai_welcome_redirect_' . $actor; if ( ! get_transient( $key ) ) { return; } delete_transient( $key ); delete_transient( 'antivirus_ai_setup_welcome_' . $actor ); // First activation opens the product. Optional connection choices come after guided setup. } else { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Allowlisted local presentation only; this navigation neither reads new consent nor sends or saves any shared information. if ( 'admin.php' !== $pagenow || 'antivirus-ai' !== self::key( $_GET, 'page' ) || 'welcome' !== self::key( $_GET, 'tab' ) || 'page' === self::key( $_GET, 'welcome_view' ) ) { return; } try { if ( self::hasConnection() ) { return; } } catch ( \Throwable $error ) { return; } set_transient( 'antivirus_ai_welcome_popup_' . $actor, 'manual', 120 ); } wp_safe_redirect( Admin::url( 'overview' ) ); exit; } public static function assets( string $hook ): void { if ( 'toplevel_page_antivirus-ai' === $hook ) { // Opt out before printing core CSS; overriding an active transition can abort navigation or trap no-JS controls. wp_dequeue_style( 'wp-view-transitions-admin' ); wp_enqueue_style( 'antivirus-ai-welcome', plugins_url( 'assets/welcome.css', ANTIVIRUS_AI_FILE ), array( 'antivirus-ai-appearance' ), ANTIVIRUS_AI_VERSION ); wp_enqueue_script( 'antivirus-ai-welcome', plugins_url( 'assets/welcome.js', ANTIVIRUS_AI_FILE ), array(), ANTIVIRUS_AI_VERSION, array( 'strategy' => 'defer', 'in_footer' => false ) ); } } /** Identifiers are sanitized and must already use their canonical spelling. */ private static function key( array $values, string $name ): string { if ( ! isset( $values[ $name ] ) || ! is_string( $values[ $name ] ) || strlen( $values[ $name ] ) > 128 ) { return ''; } $value = wp_unslash( $values[ $name ] ); $clean = sanitize_key( $value ); return strlen( $clean ) <= 64 && $clean === $value && $clean === $values[ $name ] ? $clean : ''; } private static function state(): array { $value = Store::freshOption( self::OPTION, array() ); return is_array( $value ) ? $value : array(); } private static function locked( callable $callback ): mixed { $store = new Store(); $owner = $store->lock( 'welcome_connection', 30 ); if ( ! $owner ) { return new \WP_Error( 'welcome_busy', __( 'Your choices are already being saved. Wait a moment, then retry.', 'shieldlume-security' ) ); } try { return $callback(); } finally { $store->unlock( 'welcome_connection', $owner ); } } private static function binding(): string { return hash( 'sha256', home_url( '/' ) ); } private static function edition(): string { return defined( 'ANTIVIRUS_AI_PRO_VERSION' ) ? 'pro' : 'free'; } private static function installation(): array { global $wp_version; return array( 'domain' => strtolower( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ), 'wp_version' => (string) $wp_version, 'php_version' => PHP_VERSION, 'site_language' => get_locale() ); } /** No arbitrary endpoint, redirect, response body or diagnostic credentials. */ private static function request( array $state, string $operation, array $payload ): bool { if ( ! preg_match( '/^[a-f0-9]{32}$/D', $state['id'] ?? '' ) || ! preg_match( '/^[a-f0-9]{64}$/D', $state['token'] ?? '' ) ) { return false; } $body = wp_json_encode( $payload ); if ( ! is_string( $body ) || strlen( $body ) > 2048 ) { return false; } $response = wp_safe_remote_post( 'https://antivirus.ai/newsletter-api/v1/plugin-welcome/' . $state['id'] . '/' . $operation, array( 'timeout' => 8, 'redirection' => 0, 'sslverify' => true, 'limit_response_size' => 2048, 'user-agent' => 'AntivirusAI-Welcome/1', 'headers' => array( 'Content-Type' => 'application/json', 'Accept' => 'application/json', 'Authorization' => 'Bearer ' . $state['token'] ), 'body' => $body, 'data_format' => 'body', 'cookies' => array(), ) ); return ! is_wp_error( $response ) && 202 === wp_remote_retrieve_response_code( $response ); } /** Validated explicit choices only; returns local data without making requests. */ private static function payload( array $input ): array|\WP_Error { foreach ( array( 'installation', 'newsletter' ) as $key ) { if ( isset( $input[ $key ] ) && ( ! is_string( $input[ $key ] ) || '1' !== self::key( $input, $key ) ) ) { return new \WP_Error( 'welcome_choice', __( 'Reload this screen and choose the options you want.', 'shieldlume-security' ) ); } } $installation = '1' === self::key( $input, 'installation' ); $newsletter = '1' === self::key( $input, 'newsletter' ); $email = isset( $input['email'] ) && is_string( $input['email'] ) && strlen( $input['email'] ) <= 512 ? sanitize_email( trim( wp_unslash( $input['email'] ) ) ) : ''; if ( $newsletter && ( ! is_string( $input['email'] ?? null ) || strlen( $email ) > 254 || ! is_email( $email ) || $email !== trim( wp_unslash( $input['email'] ) ) ) ) { return new \WP_Error( 'welcome_email', __( 'Enter a valid email address, or leave the email option unchecked.', 'shieldlume-security' ) ); } return array( 'consent_version' => self::CONSENT, 'installation' => $installation ? self::installation() : null, 'newsletter' => $newsletter ? array( 'email' => $email, 'edition' => self::edition() ) : null ); } private static function submit( array $input ): array|\WP_Error { $payload = self::payload( $input ); if ( is_wp_error( $payload ) ) { return $payload; } if ( null === $payload['installation'] && null === $payload['newsletter'] ) { return array( 'message' => __( 'Nothing shared. Your local tools and WordPress updates work either way.', 'shieldlume-security' ) ); } return self::locked( static function () use ( $payload ) { $state = self::state(); if ( ! empty( $state['installation'] ) || ! empty( $state['newsletter'] ) || ! empty( $state['withdraw'] ) ) { return new \WP_Error( 'welcome_existing', __( 'Review your current connection below. Stop its sharing before submitting different choices.', 'shieldlume-security' ) ); } $state = array( 'id' => bin2hex( random_bytes( 16 ) ), 'token' => bin2hex( random_bytes( 32 ) ), 'binding' => self::binding(), 'consent_version' => self::CONSENT, 'installation' => null !== $payload['installation'], 'newsletter' => null !== $payload['newsletter'], 'payload' => $payload, 'status' => 'pending', 'consented_at' => gmdate( 'c' ), 'actor' => get_current_user_id(), 'edition' => self::edition(), 'last_attempt' => time(), 'last_refresh' => 0, 'last_installation_refresh' => 0, 'last_installation_attempt' => 0 ); // Persist the idempotent request before contacting the optional service. Store::saveOption( self::OPTION, $state ); return self::enroll( $state ); } ); } private static function enroll( array $state ): array|\WP_Error { $state['last_attempt'] = time(); if ( ! empty( $state['installation'] ) ) { $state['last_installation_attempt'] = time(); } Store::saveOption( self::OPTION, $state ); if ( ! self::request( $state, 'enroll', $state['payload'] ) ) { return new \WP_Error( 'welcome_delivery', __( 'We could not confirm delivery. Your choices are saved locally; use Retry in Welcome choices. You can continue to setup or the dashboard now.', 'shieldlume-security' ) ); } $state['status'] = 'accepted'; $state['last_refresh'] = time(); $state['last_installation_refresh'] = $state['installation'] ? time() : 0; Store::saveOption( self::OPTION, $state ); self::schedule(); return array( 'message' => $state['newsletter'] ? __( 'Request accepted. Check your email and confirm if asked. Existing subscriptions and preferences are unchanged. This screen does not verify an active subscription or inbox delivery.', 'shieldlume-security' ) : __( 'Your selected installation details were accepted. No email signup was requested.', 'shieldlume-security' ) ); } private static function retry(): array|\WP_Error { return self::locked( static function () { $state = self::state(); if ( ! empty( $state['withdraw'] ) ) { return self::sendWithdrawal( $state ); } if ( 'pending' !== ( $state['status'] ?? '' ) || ( $state['binding'] ?? '' ) !== self::binding() ) { return new \WP_Error( 'welcome_retry', __( 'This request cannot be retried. Review or stop its saved sharing before making a new choice.', 'shieldlume-security' ) ); } return self::enroll( $state ); } ); } /** A stop is durable locally before the authenticated remote withdrawal attempt. */ private static function withdraw( bool $installation, bool $newsletter, bool $removal = false ): array|\WP_Error { return self::locked( static function () use ( $installation, $newsletter, $removal ) { $state = $removal ? Store::freshOption( self::OPTION, array() ) : self::state(); if ( ! is_array( $state ) ) { return new \WP_Error( 'welcome_removal_state', __( 'Saved welcome connection data could not be read safely. Restore access to that record before preparing removal.', 'shieldlume-security' ) ); } if ( empty( $state['id'] ) ) { return array( 'message' => __( 'No connection is saved.', 'shieldlume-security' ) ); } // A pending enrollment can only be cancelled in full, preventing a replay from restoring removed scopes. if ( 'pending' === ( $state['status'] ?? '' ) ) { $installation = true; $newsletter = true; } $state['withdraw'] = array( 'installation' => $installation || ! empty( $state['withdraw']['installation'] ), 'newsletter' => $newsletter || ! empty( $state['withdraw']['newsletter'] ) ); foreach ( array( 'installation', 'newsletter' ) as $scope ) { if ( $state['withdraw'][ $scope ] ) { $state[ $scope ] = false; $state['payload'][ $scope ] = null; } } $state['status'] = 'accepted'; Store::saveOption( self::OPTION, $state ); if ( ! $state['installation'] && ! $state['newsletter'] ) { self::stop(); } return self::sendWithdrawal( $state ); } ); } private static function sendWithdrawal( array $state ): array|\WP_Error { if ( ! self::request( $state, 'withdraw', $state['withdraw'] ) ) { return new \WP_Error( 'welcome_withdrawal', __( 'Sharing has stopped locally, but remote removal could not be confirmed. Use Retry to remove the saved details. To stop a confirmed email subscription, use the unsubscribe link in an email.', 'shieldlume-security' ) ); } unset( $state['withdraw'] ); if ( ! $state['installation'] && ! $state['newsletter'] ) { $state = array(); } Store::saveOption( self::OPTION, $state ); return array( 'message' => __( 'Selected sharing stopped and removal accepted. To stop a confirmed email subscription, use the unsubscribe link in an email.', 'shieldlume-security' ) ); } public static function stop(): void { wp_clear_scheduled_hook( self::HOOK ); } /** The caller must authorize explicit removal before invoking the existing local-first withdrawal. */ public static function prepareRemoval(): array|\WP_Error { try { return self::withdraw( true, true, true ); } catch ( \Throwable $error ) { return new \WP_Error( 'welcome_removal', __( 'Welcome sharing removal could not be confirmed. Keep the plugin installed and retry before removing its saved connection.', 'shieldlume-security' ) ); } } /** Fail closed if the saved credential or any pending withdrawal remains. */ public static function removalReady(): bool { try { $state = Store::freshOption( self::OPTION, array() ); return is_array( $state ) && array() === $state; } catch ( \Throwable $error ) { return false; } } /** Scheduling and local navigation send no request; only opted-in background jobs send. */ public static function schedule(): void { try { $state = self::state(); } catch ( \Throwable $error ) { return; } if ( 'accepted' !== ( $state['status'] ?? '' ) || ! empty( $state['withdraw'] ) || empty( $state['installation'] ) && empty( $state['newsletter'] ) || ( $state['binding'] ?? '' ) !== self::binding() ) { return; } $due = max( time() + 60, max( (int) ( $state['last_refresh'] ?? 0 ), (int) ( $state['last_attempt'] ?? 0 ) ) + WEEK_IN_SECONDS ); if ( ! empty( $state['newsletter'] ) && ( $state['edition'] ?? '' ) !== self::edition() ) { $due = max( time() + 60, (int) ( $state['last_attempt'] ?? 0 ) + HOUR_IN_SECONDS ); } $scheduled = wp_next_scheduled( self::HOOK ); if ( $scheduled && $scheduled <= $due ) { return; } if ( $scheduled ) { self::stop(); } wp_schedule_single_event( $due, self::HOOK ); } public static function refresh(): void { try { self::locked( static function () { $state = self::state(); if ( 'accepted' !== ( $state['status'] ?? '' ) || ! empty( $state['withdraw'] ) || empty( $state['installation'] ) && empty( $state['newsletter'] ) || ( $state['binding'] ?? '' ) !== self::binding() ) { return; } $editionChanged = ! empty( $state['newsletter'] ) && ( $state['edition'] ?? '' ) !== self::edition(); if ( time() < (int) ( $state['last_attempt'] ?? 0 ) + ( $editionChanged ? HOUR_IN_SECONDS : WEEK_IN_SECONDS ) ) { return; } $payload = array( 'consent_version' => self::CONSENT, 'installation' => $state['installation'] && time() >= max( (int) ( $state['last_installation_refresh'] ?? 0 ), (int) ( $state['last_installation_attempt'] ?? 0 ) ) + WEEK_IN_SECONDS ? self::installation() : null, 'newsletter' => $state['newsletter'] ? array( 'edition' => self::edition() ) : null ); $state['last_attempt'] = time(); if ( null !== $payload['installation'] ) { $state['last_installation_attempt'] = time(); } Store::saveOption( self::OPTION, $state ); if ( self::request( $state, 'refresh', $payload ) ) { $state['last_refresh'] = time(); $state['edition'] = self::edition(); if ( null !== $payload['installation'] ) { $state['last_installation_refresh'] = time(); } unset( $state['refresh_failed'] ); } else { $state['refresh_failed'] = true; } Store::saveOption( self::OPTION, $state ); } ); } catch ( \Throwable $error ) { /* Optional connection failures never affect local security operations. */ } try { self::schedule(); } catch ( \Throwable $error ) { /* Optional scheduling failure cannot interrupt scanning. */ } } public static function handle(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Administrator access is required.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); return; } if ( ! self::postRequest() ) { wp_die( esc_html__( 'A POST request is required.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- The original nonce is read only for type and byte-length guards; sanitize_text_field(wp_unslash()) runs here before the standalone verifier and any requested operation. $nonce = isset( $_POST['_avai_nonce'] ) && is_string( $_POST['_avai_nonce'] ) && strlen( $_POST['_avai_nonce'] ) <= 40 ? sanitize_text_field( wp_unslash( $_POST['_avai_nonce'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Original nonce bytes are used only to reject sanitizer changes; only the bounded sanitized nonce reaches the following standalone verifier. if ( '' === $nonce || strlen( $nonce ) > 20 || $nonce !== wp_unslash( $_POST['_avai_nonce'] ) ) { wp_die( esc_html__( 'Refresh this form and try again.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); return; } if ( ! wp_verify_nonce( $nonce, 'antivirus_ai_welcome' ) ) { wp_die( esc_html__( 'Refresh this form and try again.', 'shieldlume-security' ), '', array( 'response' => 403 ) ); return; } $operation = self::key( $_POST, 'welcome_operation' ); $result = null; try { switch ( $operation ) { case 'skip': $result = array( 'message' => __( 'No new information sent. Your local tools remain available. Existing saved choices, if any, are unchanged.', 'shieldlume-security' ) ); break; case 'save': $result = self::submit( $_POST ); break; case 'retry': $result = self::retry(); break; case 'stop_installation': $result = self::withdraw( true, false ); break; case 'stop_newsletter': $result = self::withdraw( false, true ); break; case 'stop_all': $result = self::withdraw( true, true ); break; default: wp_die( esc_html__( 'Choose a valid welcome action.', 'shieldlume-security' ), '', array( 'response' => 400 ) ); return; } } catch ( \Throwable $error ) { $result = new \WP_Error( 'welcome_storage', __( 'Your choices could not be confirmed. Review the saved state before retrying. Local scans, setup and the dashboard remain available.', 'shieldlume-security' ) ); } $error = is_wp_error( $result ); set_transient( 'antivirus_ai_notice_' . get_current_user_id(), array( 'kind' => $error ? 'error' : 'success', 'message' => $error ? $result->get_error_message() : $result['message'] ), 120 ); $completed = ! $error && in_array( $operation, array( 'skip', 'save' ), true ); $setup = 'setup' === self::key( $_POST, 'welcome_presentation' ); $destination = $completed ? 'overview' : ( $setup ? 'setup' : 'welcome' ); // This allowlisted context changes navigation only. Failed forms keep their honest status and saved request. $args = 'setup' === $destination ? array( 'setup_step' => 'welcome' ) : ( 'welcome' === $destination ? array( 'welcome_view' => 'page' ) : array() ); wp_safe_redirect( Admin::url( $destination, $args ) ); exit; } private static function formStart( bool $setup = false ): void { echo '
'; if ( $setup ) { echo ''; } wp_nonce_field( 'antivirus_ai_welcome', '_avai_nonce' ); } private static function control( string $operation, string $label, bool $setup = false ): void { self::formStart( $setup ); echo '
'; } public static function settings(): void { echo '

' . esc_html__( 'Welcome choices · tips and compatibility', 'shieldlume-security' ) . '

' . esc_html__( 'Optional installation sharing and email signup are separate from security alerts. Review your connection, stop sharing, or revisit the welcome screen.', 'shieldlume-security' ) . '

' . esc_html__( 'Review welcome choices', 'shieldlume-security' ) . '
'; } private static function hasConnection(): bool { $state = self::state(); return ! empty( $state['installation'] ) || ! empty( $state['newsletter'] ) || ! empty( $state['withdraw'] ); } /** Dashboard stays usable first; only an explicit settings action opens consent here. */ public static function dashboard(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } try { $actor = get_current_user_id(); $key = 'antivirus_ai_welcome_popup_' . $actor; $manual = 'manual' === get_transient( $key ); delete_transient( $key ); delete_transient( 'antivirus_ai_welcome_next_notice_' . $actor ); $needed = Onboarding::needed(); $connected = self::hasConnection(); } catch ( \Throwable $error ) { return; } // Optional local state must never suppress the real dashboard. if ( $needed ) { echo '
' . esc_html__( 'Your security workspace is ready.', 'shieldlume-security' ) . '' . esc_html__( 'Get your bearings, review readiness and choose your protection settings.', 'shieldlume-security' ) . '
' . esc_html__( 'Start guided setup', 'shieldlume-security' ) . '' . esc_html__( 'Explore dashboard', 'shieldlume-security' ) . '
'; } if ( ! $manual || $connected ) { return; } echo '
' . esc_html__( 'Optional tips and compatibility sharing', 'shieldlume-security' ) . '' . esc_html__( 'Your local security tools work either way.', 'shieldlume-security' ) . '
' . esc_html__( 'Welcome choices', 'shieldlume-security' ) . '
'; echo '

' . esc_html__( 'OPTIONAL CONNECTIONS', 'shieldlume-security' ) . '

' . esc_html__( 'Get more from Shieldlume Security.', 'shieldlume-security' ) . '

' . esc_html__( 'Useful know-how. New features. The occasional extra. Your local security tools work either way.', 'shieldlume-security' ) . '

'; self::popupForm(); echo '
'; } private static function popupForm(): void { $data = self::installation(); self::formStart(); echo '
' . esc_html__( 'Optional installation sharing', 'shieldlume-security' ) . '

' . esc_html__( 'Share your domain, WordPress/PHP versions and site language with Shieldlume Security. While enabled, these details refresh at most weekly.', 'shieldlume-security' ) . '

' . esc_html__( 'See the exact installation details', 'shieldlume-security' ) . '
'; foreach ( array( 'domain' => __( 'Site domain', 'shieldlume-security' ), 'site_language' => __( 'Site language', 'shieldlume-security' ), 'wp_version' => __( 'WordPress version', 'shieldlume-security' ), 'php_version' => __( 'PHP version', 'shieldlume-security' ) ) as $key => $label ) { echo '
' . esc_html( $label ) . '
' . esc_html( $data[ $key ] ) . '
'; } echo '
' . esc_html__( 'Optional newsletter signup', 'shieldlume-security' ) . '

' . esc_html__( 'Practical how-to guides, release news and occasional Pro offers when you’re using Free. Unsubscribe anytime.', 'shieldlume-security' ) . '

' . esc_html__( 'We use your email and this installation’s reported Free/Pro edition to tailor these messages. Your current edition is:', 'shieldlume-security' ) . ' ' . esc_html( self::edition() ) . '. ' . esc_html__( 'Edition status refreshes weekly and after changes while sharing stays enabled. Confirm your email to join; existing subscriptions and preferences stay unchanged. These are not live threat alerts.', 'shieldlume-security' ) . '

' . esc_html__( 'Your choices and privacy', 'shieldlume-security' ) . '

' . esc_html__( 'Choose either, both, or neither. Only selected details go to antivirus.ai, with a random connection ID and consent record; the service also receives the request’s network address. No names, files, findings or plugin lists are sent. Change sharing in Protection settings; use any newsletter’s unsubscribe link to stop confirmed email.', 'shieldlume-security' ) . '

' . esc_html__( 'Skipping keeps scans and WordPress updates available.', 'shieldlume-security' ) . ' ' . esc_html__( 'Privacy policy (opens in a new tab)', 'shieldlume-security' ) . '

'; } public static function render( bool $next = false ): void { // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- The brand helper returns fixed reviewed SVG/HTML; appearance helper escapes its allowlisted attribute. echo '
' . wp_kses( Experience::brand(), Experience::markupTags() ) . ''; WorkspaceDisplay::toolbar( $next ? 'welcome-next' : 'welcome', '', ! $next ); echo '
'; $notice = get_transient( 'antivirus_ai_notice_' . get_current_user_id() ); if ( is_array( $notice ) ) { delete_transient( 'antivirus_ai_notice_' . get_current_user_id() ); echo '

' . esc_html( $notice['message'] ?? '' ) . '

'; } if ( $next ) { echo '

' . esc_html__( 'YOUR WORKSPACE IS READY', 'shieldlume-security' ) . '

' . esc_html__( 'Where would you like to start?', 'shieldlume-security' ) . '

' . esc_html__( 'Review your settings with a little guidance, or go straight to your security workspace. Neither action starts a scan or enables an online service.', 'shieldlume-security' ) . '

'; self::nextLinks( true ); echo '
'; } else { self::renderChoices(); } echo '
'; } private static function nextLinks( bool $explore = false ): void { echo '
' . esc_html__( 'Start guided setup', 'shieldlume-security' ) . '' . esc_html( $explore ? __( 'Explore dashboard', 'shieldlume-security' ) : __( 'Open dashboard', 'shieldlume-security' ) ) . '
'; } /** Final wizard page: preferences already saved, optional choices remain independent and unchecked. */ public static function renderSetup(): void { if ( ! current_user_can( 'manage_options' ) ) { return; } echo '
'; Onboarding::progress( 4 ); echo '

' . esc_html__( 'Your setup preferences are saved. This last step is optional: choose either, both, or neither, then continue to your dashboard. Your local security tools work either way.', 'shieldlume-security' ) . '

'; self::renderChoices( true ); echo '
'; } private static function renderChoices( bool $setup = false ): void { $state = self::state(); if ( ! empty( $state['installation'] ) || ! empty( $state['newsletter'] ) || ! empty( $state['withdraw'] ) ) { echo '

' . esc_html__( 'Your welcome choices', 'shieldlume-security' ) . '

' . esc_html__( 'Your current permissions are shown below. Changing these choices never disables your local security tools.', 'shieldlume-security' ) . '

'; if ( ! empty( $state['newsletter'] ) ) { echo '

' . esc_html( $state['payload']['newsletter']['email'] ?? '' ) . ' — ' . esc_html__( 'Confirm your email if asked. Subscription status is not checked here.', 'shieldlume-security' ) . '

'; } if ( ( $state['binding'] ?? '' ) !== self::binding() ) { echo '

' . esc_html__( 'The site address changed. Automatic sharing is paused. Stop the old connection before making new choices for this site.', 'shieldlume-security' ) . '

'; } if ( ! empty( $state['refresh_failed'] ) || ( ! empty( $state['last_refresh'] ) && time() > (int) $state['last_refresh'] + 2 * WEEK_IN_SECONDS ) ) { echo '

' . esc_html__( 'The latest automatic update is unconfirmed or out of date. Local permissions remain saved. Stop sharing and make fresh choices if you want to reconnect; setup and scanning remain available.', 'shieldlume-security' ) . '

'; } if ( 'pending' === ( $state['status'] ?? '' ) || ! empty( $state['withdraw'] ) ) { echo '

' . esc_html__( 'The last request has not been confirmed. Setup and the dashboard are available while you resolve it.', 'shieldlume-security' ) . '

'; self::control( 'retry', __( 'Retry saved request', 'shieldlume-security' ), $setup ); } if ( 'pending' === ( $state['status'] ?? '' ) ) { self::control( 'stop_all', __( 'Cancel saved request and stop sharing', 'shieldlume-security' ), $setup ); } else { if ( ! empty( $state['installation'] ) ) { self::control( 'stop_installation', __( 'Stop installation sharing', 'shieldlume-security' ), $setup ); } if ( ! empty( $state['newsletter'] ) ) { self::control( 'stop_newsletter', __( 'Stop email and edition sharing', 'shieldlume-security' ), $setup ); } } echo '

' . esc_html__( 'Stopping installation sharing removes compatibility details. Stopping email and edition sharing cancels this connection’s pending signup; an unconfirmed delivery request is cancelled in full. A confirmed email subscription is separate: use Unsubscribe in any newsletter to stop tips, updates and offers.', 'shieldlume-security' ) . '

'; if ( $setup ) { echo '

' . esc_html__( 'Continue to dashboard', 'shieldlume-security' ) . '

'; } else { self::nextLinks(); } echo '
'; return; } $data = self::installation(); echo '

' . esc_html__( 'WELCOME TO SHIELDLUME SECURITY', 'shieldlume-security' ) . '

' . esc_html__( 'Get more from Shieldlume Security.', 'shieldlume-security' ) . '

' . esc_html__( 'Useful know-how. New features. The occasional extra. Choose what works for you — your local security tools work either way.', 'shieldlume-security' ) . '

'; self::formStart( $setup ); echo '
' . esc_html__( 'Optional installation sharing', 'shieldlume-security' ) . '

' . esc_html__( 'Share these details with Shieldlume Security so we can understand the environments our plugin needs to support. While enabled, these details refresh at most weekly.', 'shieldlume-security' ) . '

'; foreach ( array( 'domain' => __( 'Site domain', 'shieldlume-security' ), 'site_language' => __( 'Site language', 'shieldlume-security' ), 'wp_version' => __( 'WordPress version', 'shieldlume-security' ), 'php_version' => __( 'PHP version', 'shieldlume-security' ) ) as $key => $label ) { echo '
' . esc_html( $label ) . '
' . esc_html( $data[ $key ] ) . '
'; } echo '
' . esc_html__( 'Optional newsletter signup', 'shieldlume-security' ) . '

' . esc_html__( 'Get practical how-to guides, discover new features, and hear about occasional Pro offers when you’re using Free. Unsubscribe anytime.', 'shieldlume-security' ) . '

' . esc_html__( 'We use your email and this installation’s reported Free/Pro edition to tailor these messages. Your current edition is:', 'shieldlume-security' ) . ' ' . esc_html( self::edition() ) . '. ' . esc_html__( 'While enabled, edition status refreshes weekly and after edition changes. Confirm your email to join. Existing subscriptions and preferences stay unchanged. This does not subscribe you to live threat alerts.', 'shieldlume-security' ) . '

' . esc_html__( 'Choose either, both, or neither. Only your selected details are sent to antivirus.ai, with a random connection ID and consent record; the service also receives the request’s network address. No names, files, findings or plugin lists are sent. Skipping does not affect scans or WordPress plugin updates. Change sharing in Protection settings; unsubscribe through any newsletter.', 'shieldlume-security' ) . ' ' . esc_html__( 'Privacy policy (opens in a new tab)', 'shieldlume-security' ) . '

' . esc_html__( 'Next: return to your dashboard. Setup and local security tools remain available.', 'shieldlume-security' ) . '

'; } /** Only an email-matched local record; the bearer and connection ID are never exported. */ public static function privacyExport( string $email ): array { $state = self::state(); if ( '' === $email || 0 !== strcasecmp( (string) ( $state['payload']['newsletter']['email'] ?? '' ), $email ) ) { return array(); } $data = array( array( 'name' => 'Newsletter request email', 'value' => $email ), array( 'name' => 'Delivery state (not subscription status)', 'value' => $state['status'] ?? 'unknown' ), array( 'name' => 'Consent version', 'value' => $state['consent_version'] ?? '' ), array( 'name' => 'Consent time (UTC)', 'value' => $state['consented_at'] ?? '' ), array( 'name' => 'Edition sharing', 'value' => empty( $state['newsletter'] ) ? 'Off' : 'Selected' ) ); if ( ! empty( $state['installation'] ) && is_array( $state['payload']['installation'] ?? null ) ) { foreach ( array( 'domain' => 'Shared site domain', 'wp_version' => 'Shared WordPress version', 'php_version' => 'Shared PHP version', 'site_language' => 'Shared site language' ) as $key => $label ) { $data[] = array( 'name' => $label, 'value' => (string) ( $state['payload']['installation'][ $key ] ?? '' ) ); } } return array( array( 'group_id' => 'antivirus-ai-welcome', 'group_label' => 'Shieldlume Security welcome choices', 'item_id' => 'antivirus-ai-welcome-email', 'data' => $data ) ); } public static function privacyErase( string $email ): array|\WP_Error { if ( ! self::privacyExport( $email ) ) { return array( 'removed' => false ); } $result = self::withdraw( false, true ); if ( is_wp_error( $result ) ) { return $result; } return array( 'removed' => true ); } }