array( 'label' => __( 'General', 'logvia' ), 'fields' => array( 'page_id', 'replace_wp_links', 'redirect_wp_login', 'redirect_after_login', 'redirect_custom', 'redirect_after_register', 'redirect_register_custom', 'redirect_after_logout', 'redirect_logout_custom', 'delete_data' ), ), 'registration' => array( 'label' => __( 'Registration', 'logvia' ), 'fields' => array( 'registration', 'email_verification', 'min_password', 'default_role', 'reset_method' ), ), 'popup' => array( 'label' => __( 'Popup', 'logvia' ), 'fields' => array( 'popup_enabled', 'popup_style', 'popup_side', 'popup_intercept', 'popup_auto', 'popup_auto_delay', 'popup_auto_pages', 'popup_auto_once', 'popup_force' ), ), 'appearance' => array( 'label' => __( 'Appearance', 'logvia' ), 'fields' => array( 'accent', 'button_text', 'tab_active', 'tab_active_text', 'radius', 'popup_width', 'overlay_color', 'overlay_opacity', 'logo', 'layout', 'image', 'image_title', 'image_text' ), ), 'forms' => array( 'label' => __( 'Form builder', 'logvia' ), 'fields' => array(), ), 'texts' => array( 'label' => __( 'Texts & emails', 'logvia' ), 'fields' => array(), ), 'security' => array( 'label' => __( 'Security', 'logvia' ), 'fields' => array( 'captcha', 'captcha_site_key', 'captcha_secret', 'captcha_login', 'captcha_register', 'captcha_forgot', 'recaptcha_threshold', 'strength_meter', 'min_strength', 'limit_attempts', 'limit_window' ), ), 'google' => array( 'label' => __( 'Google sign-in', 'logvia' ), 'fields' => array( 'google_enabled', 'google_client_id', 'google_client_secret', 'google_register', 'google_allow_privileged' ), ), 'woocommerce' => array( 'label' => __( 'WooCommerce', 'logvia' ), 'fields' => array( 'wc_replace_myaccount', 'wc_checkout_login', 'gate_enabled', 'gate_notice' ), ), ); /** * Filter the settings tabs and the fields each one saves. Add-ons * append their own field keys to a tab here. * * @param array $tabs Tabs. */ return apply_filters( 'logvia_settings_tabs', $tabs ); } /** * Checkbox fields (missing from POST means off). * * @return string[] */ private static function checkboxes() { return apply_filters( 'logvia_settings_checkboxes', array( 'registration', 'email_verification', 'replace_wp_links', 'redirect_wp_login', 'delete_data', 'popup_enabled', 'popup_intercept', 'popup_auto', 'popup_auto_once', 'popup_force', 'captcha_login', 'captcha_register', 'captcha_forgot', 'strength_meter', 'google_enabled', 'google_register', 'google_allow_privileged', 'wc_replace_myaccount', 'wc_checkout_login', 'gate_enabled' ) ); } /** * Allowed values for select/radio fields. * * @return array */ private static function choices() { $choices = array( 'redirect_after_login' => array( 'account', 'home', 'same', 'custom' ), 'redirect_after_register' => array( 'login', 'account', 'home', 'same', 'custom' ), 'redirect_after_logout' => array( 'login', 'home', 'same', 'custom' ), 'reset_method' => array( 'link', 'code' ), 'popup_style' => array( 'modal', 'slide' ), 'popup_side' => array( 'right', 'left' ), 'layout' => array( 'card', 'split' ), 'captcha' => array( 'none', 'turnstile', 'recaptcha_v2', 'recaptcha_v3' ), 'min_strength' => array( '0', '2', '3' ), ); return apply_filters( 'logvia_settings_choices', $choices ); } /** * Menu entry. */ public static function menu() { add_options_page( __( 'Logvia', 'logvia' ), __( 'Logvia', 'logvia' ), 'manage_options', 'logvia', array( __CLASS__, 'render' ) ); } /** * Media picker on the Appearance tab. * * @param string $hook Hook. */ public static function assets( $hook ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab check only. if ( 'settings_page_logvia' === $hook && isset( $_GET['tab'] ) && 'appearance' === $_GET['tab'] ) { wp_enqueue_media(); wp_add_inline_script( 'media-editor', "document.addEventListener('click',function(e){var b=e.target.closest('[data-logvia-media]');if(!b)return;e.preventDefault();var f=wp.media({library:{type:'image'},multiple:false});f.on('select',function(){var a=f.state().get('selection').first().toJSON();document.getElementById(b.getAttribute('data-logvia-media')).value=a.url;});f.open();});" ); } } /** * Register the option. */ public static function register() { register_setting( 'logvia', Logvia_Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( __CLASS__, 'sanitize' ), ) ); } /** * Sanitize the fields of the submitted tab and keep the rest. * * @param mixed $input Submitted values. * @return array */ public static function sanitize( $input ) { $input = is_array( $input ) ? $input : array(); $old = Logvia_Settings::all(); $tabs = self::tabs(); $tab = isset( $input['_tab'] ) && ! empty( $tabs[ $input['_tab'] ]['fields'] ) ? $input['_tab'] : ''; if ( ! $tab ) { // Programmatic update_option() call: keep known keys only. return array_intersect_key( wp_parse_args( $input, $old ), Logvia_Settings::defaults() ); } $choices = self::choices(); $out = $old; foreach ( $tabs[ $tab ]['fields'] as $field ) { $value = isset( $input[ $field ] ) ? $input[ $field ] : null; if ( in_array( $field, self::checkboxes(), true ) ) { $out[ $field ] = empty( $value ) ? 0 : 1; continue; } if ( isset( $choices[ $field ] ) ) { $value = (string) $value; $out[ $field ] = in_array( $value, $choices[ $field ], true ) ? ( is_numeric( $value ) ? (int) $value : $value ) : Logvia_Settings::defaults()[ $field ]; continue; } switch ( $field ) { case 'page_id': $out[ $field ] = absint( $value ); break; case 'min_password': $out[ $field ] = min( 64, max( 6, absint( $value ) ) ); break; case 'radius': $out[ $field ] = min( 30, absint( $value ) ); break; case 'popup_width': $out[ $field ] = min( 900, max( 320, absint( $value ) ) ); break; case 'overlay_opacity': $out[ $field ] = min( 100, absint( $value ) ); break; case 'popup_auto_delay': $out[ $field ] = min( 120, absint( $value ) ); break; case 'limit_attempts': $out[ $field ] = min( 50, max( 1, absint( $value ) ) ); break; case 'limit_window': $out[ $field ] = min( 1440, max( 1, absint( $value ) ) ); break; case 'recaptcha_threshold': $out[ $field ] = min( 1, max( 0, round( (float) $value, 1 ) ) ); break; case 'default_role': $value = sanitize_key( (string) $value ); $out[ $field ] = array_key_exists( $value, Logvia_Forms::assignable_roles() ) ? $value : ''; break; case 'accent': case 'button_text': case 'tab_active': case 'tab_active_text': case 'overlay_color': $color = sanitize_hex_color( (string) $value ); $out[ $field ] = $color ? $color : Logvia_Settings::defaults()[ $field ]; break; case 'redirect_custom': case 'redirect_register_custom': case 'redirect_logout_custom': case 'image': case 'logo': $out[ $field ] = esc_url_raw( trim( (string) $value ) ); break; case 'image_text': $out[ $field ] = sanitize_textarea_field( (string) $value ); break; case 'popup_auto_pages': $out[ $field ] = implode( ',', array_filter( array_map( 'sanitize_title', explode( ',', (string) $value ) ) ) ); break; default: $out[ $field ] = sanitize_text_field( (string) $value ); } } return $out; } /** * Settings page. */ public static function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tabs = self::tabs(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab switch only. $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; $tab = isset( $tabs[ $tab ] ) ? $tab : 'general'; $s = Logvia_Settings::all(); ?>

'; return; } if ( 'texts' === $tab ) { Logvia_Forms_Admin::render_texts(); echo '
'; return; } ?>

>

label. * @param string $desc Description. */ public static function select( $s, $key, $title, $options, $desc = '' ) { $id = 'logvia-' . $key; ?>

' . esc_html( $title ) . '

'; } /* ------------------------------------------------------------------ * Tabs * ------------------------------------------------------------------ */ /** * General tab. * * @param array $s Settings. */ private static function tab_general( $s ) { $page = Logvia_Plugin::page_id(); ?> esc_attr( self::name( 'page_id' ) ), 'id' => 'logvia-page_id', 'selected' => (int) $s['page_id'], 'show_option_none' => esc_html__( '— Select —', 'logvia' ), 'option_none_value' => '0', ) ); ?>

__( 'My Account page', 'logvia' ), 'home' => __( 'Home page', 'logvia' ), 'same' => __( 'Stay on the same page', 'logvia' ), 'custom' => __( 'Custom URL', 'logvia' ), ); self::select( $s, 'redirect_after_login', __( 'After login', 'logvia' ), $common, __( 'Used when the visitor did not come from another page (for example a product they wanted to buy).', 'logvia' ) ); self::text( $s, 'redirect_custom', __( 'Custom URL after login', 'logvia' ), '', 'url', 'large-text' ); self::select( $s, 'redirect_after_register', __( 'After registration', 'logvia' ), array( 'login' => __( 'Same as after login', 'logvia' ) ) + $common ); self::text( $s, 'redirect_register_custom', __( 'Custom URL after registration', 'logvia' ), '', 'url', 'large-text' ); self::select( $s, 'redirect_after_logout', __( 'After logout', 'logvia' ), array( 'login' => __( 'Login screen', 'logvia' ), 'home' => __( 'Home page', 'logvia' ), 'same' => __( 'Stay on the same page', 'logvia' ), 'custom' => __( 'Custom URL', 'logvia' ), ) ); self::text( $s, 'redirect_logout_custom', __( 'Custom URL after logout', 'logvia' ), '', 'url', 'large-text' ); self::section( __( 'Data', 'logvia' ) ); self::checkbox( $s, 'delete_data', __( 'Uninstall', 'logvia' ), __( 'Delete Logvia settings and the account page when the plugin is deleted', 'logvia' ) ); } /** * Registration tab. * * @param array $s Settings. */ private static function tab_registration( $s ) { self::checkbox( $s, 'registration', __( 'Registration', 'logvia' ), __( 'Let visitors create accounts', 'logvia' ) ); self::checkbox( $s, 'email_verification', __( 'Email verification', 'logvia' ), __( 'Confirm the email address with a six-digit code before the account is created', 'logvia' ), __( 'Recommended. It keeps fake and mistyped accounts out of your store.', 'logvia' ) ); self::text( $s, 'min_password', __( 'Minimum password length', 'logvia' ), '', 'number', 'small-text' ); $roles = array( '' => __( 'Default (Customer with WooCommerce)', 'logvia' ) ) + Logvia_Forms::assignable_roles(); self::select( $s, 'default_role', __( 'Role of new accounts', 'logvia' ), $roles, __( 'Only roles without admin, editing or shop-management rights can be chosen.', 'logvia' ) ); self::select( $s, 'reset_method', __( 'Password reset', 'logvia' ), array( 'link' => __( 'Email a reset link', 'logvia' ), 'code' => __( 'Email a six-digit code (customers stay on the page)', 'logvia' ), ) ); } /** * Popup tab. * * @param array $s Settings. */ private static function tab_popup( $s ) { self::checkbox( $s, 'popup_enabled', __( 'Popup', 'logvia' ), __( 'Open login and registration in a popup', 'logvia' ), __( 'Guests can log in without leaving the page they are on.', 'logvia' ) ); self::select( $s, 'popup_style', __( 'Style', 'logvia' ), array( 'modal' => __( 'Centered window', 'logvia' ), 'slide' => __( 'Slide-in panel', 'logvia' ), ) ); self::select( $s, 'popup_side', __( 'Panel side', 'logvia' ), array( 'right' => __( 'Right', 'logvia' ), 'left' => __( 'Left', 'logvia' ), ) ); self::checkbox( $s, 'popup_intercept', __( 'Links', 'logvia' ), __( 'Open the popup when visitors click any link to the login or registration screen', 'logvia' ) ); ?> ' . esc_html__( 'Choose image', 'logvia' ) . ''; self::text( $s, 'logo', __( 'Logo', 'logvia' ), __( 'Shown above the forms instead of the icon.', 'logvia' ), 'url', 'regular-text', sprintf( $pick, 'logo' ) ); self::select( $s, 'layout', __( 'Page layout', 'logvia' ), array( 'card' => __( 'Centered card', 'logvia' ), 'split' => __( 'Image on the left, form on the right', 'logvia' ), ), __( 'The split layout needs an image and works best on full-width page templates.', 'logvia' ) ); self::text( $s, 'image', __( 'Side image', 'logvia' ), __( 'Portrait images of at least 1200×1500 px look best.', 'logvia' ), 'url', 'regular-text', sprintf( $pick, 'image' ) ); self::text( $s, 'image_title', __( 'Image title', 'logvia' ) ); ?> __( 'Off', 'logvia' ), 'turnstile' => __( 'Cloudflare Turnstile (recommended, privacy-friendly)', 'logvia' ), 'recaptcha_v2' => __( 'Google reCAPTCHA v2 (checkbox)', 'logvia' ), 'recaptcha_v3' => __( 'Google reCAPTCHA v3 (invisible)', 'logvia' ), ) ); self::text( $s, 'captcha_site_key', __( 'Site key', 'logvia' ), '', 'text', 'large-text' ); self::text( $s, 'captcha_secret', __( 'Secret key', 'logvia' ), __( 'Stored on your server only.', 'logvia' ), 'password', 'large-text' ); ?> __( 'Login', 'logvia' ), 'captcha_register' => __( 'Registration', 'logvia' ), 'captcha_forgot' => __( 'Lost password', 'logvia' ), ) as $key => $label ) : ?> __( 'No requirement (length only)', 'logvia' ), '2' => __( 'At least medium', 'logvia' ), '3' => __( 'At least strong', 'logvia' ), ) ); self::section( __( 'Login attempts', 'logvia' ) ); self::text( $s, 'limit_attempts', __( 'Failed logins allowed', 'logvia' ), __( 'Per account. Each IP address may try four times as many accounts.', 'logvia' ), 'number', 'small-text' ); self::text( $s, 'limit_window', __( 'Lockout time (minutes)', 'logvia' ), '', 'number', 'small-text' ); } /** * Google tab. * * @param array $s Settings. */ private static function tab_google( $s ) { ?>

' . esc_html__( 'Install and activate WooCommerce to use these options.', 'logvia' ) . '

'; } self::checkbox( $s, 'wc_replace_myaccount', __( 'My Account', 'logvia' ), __( 'Replace the WooCommerce login/register form with Logvia', 'logvia' ), __( 'Logged-out visitors of My Account are sent to the Logvia login screen and brought back afterwards.', 'logvia' ) ); self::checkbox( $s, 'wc_checkout_login', __( 'Checkout', 'logvia' ), __( 'Replace the "Returning customer? Click here to login" box on the checkout', 'logvia' ), __( 'Uses the popup when it is on, otherwise an inline form. Needs "Allow customers to log into an existing account during checkout" in WooCommerce settings.', 'logvia' ) ); self::section( __( 'Require an account to check out', 'logvia' ) ); self::checkbox( $s, 'gate_enabled', __( 'Require login', 'logvia' ), __( 'Ask visitors to log in or register before checkout', 'logvia' ), __( 'Guests can fill a cart; at checkout they log in and come straight back with the cart kept.', 'logvia' ) ); self::text( $s, 'gate_notice', __( 'Message on the login screen', 'logvia' ), __( 'Leave empty for the default: "Please log in or create an account to place an order."', 'logvia' ), 'text', 'large-text' ); } /* ------------------------------------------------------------------ * Misc * ------------------------------------------------------------------ */ /** * Create the account page from the settings screen. */ public static function create_page() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do that.', 'logvia' ) ); } check_admin_referer( 'logvia_create_page' ); Logvia_Plugin::ensure_page(); wp_safe_redirect( admin_url( 'options-general.php?page=logvia' ) ); exit; } /** * Warnings. */ public static function notices() { if ( ! current_user_can( 'manage_options' ) ) { return; } if ( ! Logvia_Plugin::page_id() ) { echo '

' . esc_html__( 'Logvia needs an account page to show its login and registration screens.', 'logvia' ) . ' ' . esc_html__( 'Open settings', 'logvia' ) . '

'; } if ( Logvia_Settings::get( 'google_enabled' ) && ! Logvia_Google::enabled() && Logvia_Plugin::page_id() ) { echo '

' . esc_html__( 'Logvia: Google sign-in is switched on but the Client ID or Client secret is missing.', 'logvia' ) . '

'; } if ( 'none' !== Logvia_Settings::get( 'captcha' ) && 'none' === Logvia_Captcha::provider() ) { echo '

' . esc_html__( 'Logvia: a captcha is selected but its site key or secret key is missing, so it is not active.', 'logvia' ) . '

'; } } /** * "Settings" link on the Plugins screen. * * @param array $links Links. * @return array */ public static function action_links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'logvia' ) . '' ); return $links; } /** * Label the account page in the Pages list. * * @param array $states States. * @param WP_Post $post Post. * @return array */ public static function post_state( $states, $post ) { if ( (int) $post->ID === (int) Logvia_Settings::get( 'page_id' ) ) { $states['logvia'] = __( 'Logvia account page', 'logvia' ); } return $states; } }