array(), 'show_first_name' => true, 'show_last_name' => false, 'double_opt_in' => true, 'consent_enabled' => false, 'consent_text' => __( 'I agree to receive email newsletters.', 'mowl-email' ), 'success_message' => __( 'Thank you, you are subscribed!', 'mowl-email' ), 'pending_message' => __( 'Almost done! Please check your inbox and confirm your subscription.', 'mowl-email' ), 'error_message' => __( 'Something went wrong. Please try again later.', 'mowl-email' ), 'button_label' => __( 'Subscribe', 'mowl-email' ), 'confirm_redirect_url' => '', 'behind_cloudflare' => false, 'turnstile_enabled' => false, 'turnstile_site_key' => '', 'turnstile_secret' => '', ); } public static function load(): array { return array_merge( self::defaults(), Options::get_array( Options::FORM ) ); } /** * A Renderer ezt írja a form `_mowl_sig` mezőjébe, a `_mowl_cfg` (base64 * JSON) blob pontos szövegére számolva; a handler csak akkor fogadja el a * form által küldött felülírásokat, ha az aláírás egyezik — különben a * `_mowl_cfg` egy egyszerű rejtett input, egy szkriptelt POST bármit * küldhetne benne. A blob csak a shortcode/block felülírásait hordozza * (lásd overrides()), így az admin alapértékei mentés után azonnal * érvényesek minden olyan formon, amely nem írja felül őket. */ public static function signature( string $cfg ): string { return wp_hash( $cfg ); } /** Az admin form nyers POST-jából sanitizált beállítás; ment és visszaad. */ public static function save( array $raw ): array { $clean = array( 'tags' => self::int_list( $raw['tags'] ?? array() ), 'show_first_name' => ! empty( $raw['show_first_name'] ), 'show_last_name' => ! empty( $raw['show_last_name'] ), 'double_opt_in' => ! empty( $raw['double_opt_in'] ), 'consent_enabled' => ! empty( $raw['consent_enabled'] ), 'consent_text' => self::text( $raw['consent_text'] ?? '', 'consent_text' ), 'success_message' => self::text( $raw['success_message'] ?? '', 'success_message' ), 'pending_message' => self::text( $raw['pending_message'] ?? '', 'pending_message' ), 'error_message' => self::text( $raw['error_message'] ?? '', 'error_message' ), 'button_label' => self::text( $raw['button_label'] ?? '', 'button_label' ), 'confirm_redirect_url' => self::sanitize_redirect_url( (string) ( $raw['confirm_redirect_url'] ?? '' ) ), 'behind_cloudflare' => ! empty( $raw['behind_cloudflare'] ), ); // Turnstile: üresen hagyott secret-mező a tároltat tartja meg; a // secret titkosítva, sodium nélkül egyáltalán nem tárolódik. $site_key = trim( sanitize_text_field( (string) ( $raw['turnstile_site_key'] ?? '' ) ) ); $new_secret = trim( sanitize_text_field( (string) ( $raw['turnstile_secret'] ?? '' ) ) ); $secret = '' !== $new_secret ? ( Crypto::available() ? Crypto::encrypt( $new_secret ) : '' ) : (string) ( self::load()['turnstile_secret'] ?? '' ); // A defaults() kulcssorrendjében, hogy a load() ugyanazt a tömböt adja vissza. $clean['turnstile_enabled'] = ! empty( $raw['turnstile_enabled'] ) && '' !== $site_key && '' !== $secret && Crypto::available(); $clean['turnstile_site_key'] = $site_key; $clean['turnstile_secret'] = $secret; Options::unflag( Turnstile::MISCONFIGURED_FLAG ); Options::unflag( Turnstile::FAILING_FLAG ); Options::set( Options::FORM, $clean ); return $clean; } /** Shortcode/block attribútumok → kész beállítás (admin alapérték + felülírások). */ public static function resolve( array $attrs, Connection $connection ): array { return array_merge( self::load(), self::overrides( $attrs, $connection ) ); } /** * Csak a ténylegesen megadott attribútumokból származó beállításkulcsok. * Attribútumok: tags (vesszős név vagy id lista), fields * (email,first_name,last_name), doi (0|1), button, success, pending, * redirect, consent (0|1), consent_text. */ public static function overrides( array $attrs, Connection $connection ): array { $settings = array(); if ( self::given( $attrs, 'tags' ) ) { // Csak a tárolt címkelistából: a front-end render-útvonalon nem // indíthatunk kifelé HTTP-hívást (lásd Connection::known_tags()). $settings['tags'] = self::resolve_tags( (string) $attrs['tags'], $connection->known_tags() ?? array() ); } if ( self::given( $attrs, 'fields' ) ) { $fields = array_map( 'trim', explode( ',', strtolower( (string) $attrs['fields'] ) ) ); $settings['show_first_name'] = in_array( 'first_name', $fields, true ); $settings['show_last_name'] = in_array( 'last_name', $fields, true ); } if ( isset( $attrs['doi'] ) && '' !== (string) $attrs['doi'] ) { $settings['double_opt_in'] = self::truthy( $attrs['doi'] ); } if ( isset( $attrs['consent'] ) && '' !== (string) $attrs['consent'] ) { $settings['consent_enabled'] = self::truthy( $attrs['consent'] ); } foreach ( array( 'button' => 'button_label', 'success' => 'success_message', 'pending' => 'pending_message', 'consent_text' => 'consent_text', ) as $attr => $key ) { if ( self::given( $attrs, $attr ) ) { $settings[ $key ] = self::text( (string) $attrs[ $attr ], $key ); } } if ( self::given( $attrs, 'redirect' ) ) { $settings['confirm_redirect_url'] = self::sanitize_redirect_url( (string) $attrs['redirect'] ); } return $settings; } /** Meg van-e adva az attribútum nem üres (trimmelt) értékkel. */ private static function given( array $attrs, string $key ): bool { return isset( $attrs[ $key ] ) && '' !== trim( (string) $attrs[ $key ] ); } /** * Csak abszolút http(s) URL-t fogadunk el, amelynek host-ja megegyezik a * site host-jával — különben a `confirm_redirect_url` egy nyílt * átirányítás (open redirect) mellékcsatorna lenne egy admin/form * beállításon vagy egy aláírt (de a szerveren kívülről befolyásolható) * `redirect` shortcode-attribútumon keresztül. */ public static function sanitize_redirect_url( string $url ): string { $url = esc_url_raw( trim( $url ) ); if ( '' === $url ) { return ''; } $scheme = wp_parse_url( $url, PHP_URL_SCHEME ); if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) { return ''; } $host = wp_parse_url( $url, PHP_URL_HOST ); $home_host = wp_parse_url( home_url(), PHP_URL_HOST ); if ( ! is_string( $host ) || ! is_string( $home_host ) || strtolower( $host ) !== strtolower( $home_host ) ) { return ''; } return $url; } /** @param array $known */ private static function resolve_tags( string $csv, array $known ): array { $by_name = array(); $known_ids = array(); foreach ( $known as $tag ) { $id = (int) $tag['id']; $name = self::lower( trim( (string) $tag['name'] ) ); $by_name[ $name ] = $id; $known_ids[] = $id; } $ids = array(); foreach ( explode( ',', $csv ) as $item ) { $item = trim( $item ); if ( '' === $item ) { continue; } if ( ctype_digit( $item ) ) { // Numerikus id: ha van cache-elt tag-lista, csak az abban // szereplő id-ket fogadjuk el; cache nélkül a backend úgyis // validál. $id = (int) $item; if ( empty( $known ) || in_array( $id, $known_ids, true ) ) { $ids[] = $id; } } elseif ( isset( $by_name[ self::lower( $item ) ] ) ) { $ids[] = $by_name[ self::lower( $item ) ]; } } return array_values( array_unique( $ids ) ); } /** A WP core csak mb_strlen/mb_substr-t polyfillel, mb_strtolower-t nem. */ private static function lower( string $s ): string { return function_exists( 'mb_strtolower' ) ? mb_strtolower( $s, 'UTF-8' ) : strtolower( $s ); } /** @param mixed $value */ private static function truthy( $value ): bool { return in_array( strtolower( (string) $value ), array( '1', 'true', 'yes', 'on' ), true ); } /** * Pozitív egész id-k listája (tömbből vagy vesszős szövegből), duplikátum nélkül. * * @param mixed $raw */ public static function int_list( $raw ): array { $raw = is_array( $raw ) ? $raw : explode( ',', (string) $raw ); $ids = array(); foreach ( $raw as $v ) { if ( is_numeric( $v ) && (int) $v > 0 ) { $ids[] = (int) $v; } } return array_values( array_unique( $ids ) ); } private static function text( string $value, string $key ): string { $value = trim( sanitize_text_field( $value ) ); return '' === $value ? (string) self::defaults()[ $key ] : $value; } }