'array', 'sanitize_callback' => [ Settings::class, 'sanitize' ], ] ); } ); } /** * Settings tabs. Add-ons add tabs with the ndsg_settings_tabs filter, and * either fields (ndsg_settings_fields) or their own screen * (action ndsg_settings_tab_{tab}). */ private function tabs() { return (array) apply_filters( 'ndsg_settings_tabs', [ 'general' => __( 'Device limit', 'netdesign-session-guard' ), 'detect' => __( 'Detection', 'netdesign-session-guard' ), 'advanced' => __( 'Advanced', 'netdesign-session-guard' ), ] ); } /** * @return array key => [type, label, options?, help?]; types: number, text, * email, url, textarea, checkbox, checkboxes, radio, select. */ private function fields( $tab ) { return (array) apply_filters( 'ndsg_settings_fields', $this->core_fields( $tab ), $tab ); } private function core_fields( $tab ) { $levels = []; foreach ( Risk::levels() as $level => $info ) { if ( $level > Risk::OK && '' !== $info[2] ) { $levels[ $level ] = $info[1]; } } $roles = []; foreach ( wp_roles()->get_names() as $key => $name ) { $roles[ $key ] = translate_user_role( $name ); } switch ( $tab ) { case 'general': return [ 'mode' => [ 'radio', __( 'Mode', 'netdesign-session-guard' ), [ 'monitor' => __( 'Monitor only: track and flag, never sign anyone out', 'netdesign-session-guard' ), 'enforce' => __( 'Enforce: when a user signs in on a new device beyond the limit, the oldest device is signed out immediately', 'netdesign-session-guard' ), ] ], 'max_devices' => [ 'number', __( 'Devices allowed at once', 'netdesign-session-guard' ), null, __( 'Tabs in the same browser count as one device.', 'netdesign-session-guard' ) ], 'exempt_users' => [ 'textarea', __( 'Excluded users', 'netdesign-session-guard' ), null, __( 'Emails, usernames or user IDs, one per line or separated by commas. Test accounts and staff listed here are not monitored and never limited (the same list as the Excluded users tab).', 'netdesign-session-guard' ) ], 'exempt_roles' => [ 'checkboxes', __( 'Roles without limits', 'netdesign-session-guard' ), $roles, __( 'Not limited and never flagged.', 'netdesign-session-guard' ) ], 'kick_message' => [ 'textarea', __( 'Message shown to the signed-out device', 'netdesign-session-guard' ) ], 'kick_redirect' => [ 'url', __( 'Send signed-out device to', 'netdesign-session-guard' ), null, __( 'Empty = login page.', 'netdesign-session-guard' ) ], ]; case 'detect': return [ 'monitoring' => [ 'checkbox', __( 'Monitoring', 'netdesign-session-guard' ), null, __( 'Collects activity (overlaps, and viewing with Pro) and rates it. Turning it off stops collecting new data; nothing is deleted. The device limit keeps working.', 'netdesign-session-guard' ) ], 'grace_seconds' => [ 'number', __( 'Grace period (seconds)', 'netdesign-session-guard' ), null, __( 'How long two devices must be active together before it counts. A quick move from the phone to the computer is not flagged.', 'netdesign-session-guard' ) ], 'flag_level' => [ 'select', __( 'Add accounts to the list from risk level', 'netdesign-session-guard' ), $levels, __( 'Accounts at this level or higher appear under Flagged accounts.', 'netdesign-session-guard' ) ], /* translators: %s: risk level */ 'repeat_count' => [ 'number', __( '"Very strong" when it happens (times in 7 days)', 'netdesign-session-guard' ), null, sprintf( __( 'Counts overlaps at level "%s" or higher.', 'netdesign-session-guard' ), Risk::label( Risk::repeat_level() ) ) ], 'window_days' => [ 'number', __( 'How many days back to check', 'netdesign-session-guard' ), null, __( 'The account list and the user pages count overlaps from this period.', 'netdesign-session-guard' ) ], 'dismiss_days' => [ 'number', __( 'After "checked, OK", don\'t flag the account again for (days)', 'netdesign-session-guard' ), null, __( '0 = flag it again at the next new overlap. Either way, only overlaps after "checked, OK" count.', 'netdesign-session-guard' ) ], ]; case 'advanced': return [ 'ping_interval' => [ 'number', __( 'Heartbeat every (seconds)', 'netdesign-session-guard' ), null, __( 'How fast a signed-out device notices. Only runs while the tab is visible.', 'netdesign-session-guard' ) ], 'active_window' => [ 'number', __( 'Online if seen within (seconds)', 'netdesign-session-guard' ) ], 'ip_header' => [ 'select', __( 'Client IP from', 'netdesign-session-guard' ), [ 'auto' => __( 'Automatic (recommended): detects Cloudflare and local proxies', 'netdesign-session-guard' ), 'REMOTE_ADDR' => 'REMOTE_ADDR', 'HTTP_CF_CONNECTING_IP' => 'CF-Connecting-IP (Cloudflare)', 'HTTP_X_FORWARDED_FOR' => 'X-Forwarded-For', 'HTTP_X_REAL_IP' => 'X-Real-IP', ], __( 'Automatic trusts a forwarding header only when the request really comes from Cloudflare or a local proxy, so visitors cannot fake their IP.', 'netdesign-session-guard' ) ], 'cloudflare_updates' => [ 'checkbox', __( 'Update Cloudflare IP ranges', 'netdesign-session-guard' ), null, __( 'Once a day, downloads Cloudflare\'s public list of IP ranges from api.cloudflare.com. Off: the list built into the plugin is used. Nothing about your site or users is sent.', 'netdesign-session-guard' ) ], 'anonymize_ip' => [ 'checkbox', __( 'Store anonymized IPs', 'netdesign-session-guard' ) ], 'retention_days' => [ 'number', __( 'Keep session history for (days)', 'netdesign-session-guard' ) ], ]; } return []; } public function render() { $tabs = $this->tabs(); $tab = sanitize_key( wp_unslash( $_GET['tab'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- tab navigation. $tab = isset( $tabs[ $tab ] ) ? $tab : 'general'; $base = admin_url( 'admin.php?page=' . self::SLUG ); ?>

fields( $tab ) as $key => $field ) : ?>

__( 'direct connection (REMOTE_ADDR)', 'netdesign-session-guard' ), 'HTTP_CF_CONNECTING_IP' => __( 'Cloudflare (CF-Connecting-IP)', 'netdesign-session-guard' ), 'CLOUDFLARE_RESTORED' => __( 'Cloudflare (real IP restored by the web server)', 'netdesign-session-guard' ), 'HTTP_X_FORWARDED_FOR' => __( 'local proxy (X-Forwarded-For)', 'netdesign-session-guard' ), 'HTTP_X_REAL_IP' => __( 'local proxy (X-Real-IP)', 'netdesign-session-guard' ), ]; printf( '

%s %s %s

', $cf ? 'is-ok' : '', /* translators: %s: detected source */ esc_html( sprintf( __( 'Detected: %s', 'netdesign-session-guard' ), $labels[ $source ] ?? $source ) ), esc_html__( 'Your IP as seen by Automatic:', 'netdesign-session-guard' ), esc_html( $ip ?: '—' ) ); } private function field( $key, array $f ) { $type = $f[0]; $options = $f[2] ?? []; $help = $f[3] ?? ''; $value = 'exempt_users' === $key ? \NetDesign\SessionGuard\Policy\Enforcer::whitelist_text() : Settings::get( $key ); $name = Settings::OPTION . '[' . $key . ']'; $id = 'ndsg-' . $key; switch ( $type ) { case 'number': printf( '', 'dismiss_days' === $key ? 0 : 1, esc_attr( $id ), esc_attr( $name ), (int) $value ); break; case 'text': case 'email': case 'url': printf( '', esc_attr( $type ), esc_attr( $id ), esc_attr( $name ), esc_attr( $value ) ); break; case 'textarea': printf( '', esc_attr( $id ), esc_attr( $name ), esc_textarea( $value ) ); break; case 'checkbox': // Hidden 0 so an unchecked box is submitted (settings merge across tabs). printf( '', esc_attr( $name ), esc_attr( $id ), esc_attr( $name ), checked( $value, 1, false ), esc_html__( 'Enabled', 'netdesign-session-guard' ) ); break; case 'checkboxes': printf( '', esc_attr( $name ) ); foreach ( $options as $opt => $label ) { printf( '', esc_attr( $name ), esc_attr( $opt ), checked( in_array( $opt, (array) $value, true ), true, false ), esc_html( $label ) ); } break; case 'radio': foreach ( $options as $opt => $label ) { printf( '', esc_attr( $name ), esc_attr( $opt ), checked( $value, $opt, false ), esc_html( $label ) ); } break; case 'select': printf( ''; break; } if ( $help ) { printf( '

%s

', esc_html( $help ) ); } if ( 'ip_header' === $key ) { $this->ip_detection(); } } }