array( __( 'Comments', 'rabbitproof' ), __( 'Keeps junk comments off your posts and pages.', 'rabbitproof' ), 'comment', true ), 'signups' => array( __( 'Sign-ups and lost password', 'rabbitproof' ), __( 'Stops bots creating fake accounts, and using the lost password form.', 'rabbitproof' ), 'user', true ), 'login' => array( __( 'Sign-ins', 'rabbitproof' ), __( 'Stops bots guessing passwords on your sign-in page. Signed-in administrators are never checked.', 'rabbitproof' ), 'lock', true ), 'woocommerce' => array( __( 'WooCommerce checkout', 'rabbitproof' ), __( 'Stops fake orders and fake customer accounts in your shop.', 'rabbitproof' ), 'cart', class_exists( 'WooCommerce' ) ), 'cf7' => array( __( 'Contact Form 7', 'rabbitproof' ), __( 'Keeps spam out of forms made with Contact Form 7.', 'rabbitproof' ), 'mail', defined( 'WPCF7_VERSION' ) ), 'wpforms' => array( __( 'WPForms', 'rabbitproof' ), __( 'Keeps spam out of forms made with WPForms.', 'rabbitproof' ), 'forms', function_exists( 'wpforms' ) ), 'gravity' => array( __( 'Gravity Forms', 'rabbitproof' ), __( 'Keeps spam out of forms made with Gravity Forms.', 'rabbitproof' ), 'forms', class_exists( 'GFForms' ) ), 'elementor' => array( __( 'Elementor forms', 'rabbitproof' ), __( 'Keeps spam out of forms made with Elementor.', 'rabbitproof' ), 'forms', defined( 'ELEMENTOR_PRO_VERSION' ) ), 'fluent' => array( __( 'Fluent Forms', 'rabbitproof' ), __( 'Keeps spam out of forms made with Fluent Forms.', 'rabbitproof' ), 'forms', defined( 'FLUENTFORM' ) ), ); } public static function register() { add_action( 'admin_menu', array( __CLASS__, 'menu' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); add_action( 'admin_post_rabbitproof_connect', array( __CLASS__, 'connect' ) ); add_action( 'admin_post_rabbitproof_connected', array( __CLASS__, 'connected' ) ); add_action( 'admin_post_nopriv_rabbitproof_connected', array( __CLASS__, 'connected_signin' ) ); add_action( 'admin_post_rabbitproof_cleanup', array( __CLASS__, 'remove_leftover' ) ); add_action( 'admin_post_rabbitproof_verify', array( __CLASS__, 'verify_again' ) ); add_action( 'admin_post_rabbitproof_protect', array( __CLASS__, 'save_protect' ) ); add_action( 'admin_post_rabbitproof_server', array( __CLASS__, 'save_server' ) ); add_action( 'admin_post_rabbitproof_block', array( __CLASS__, 'block_again' ) ); add_action( 'admin_post_rabbitproof_unblock', array( __CLASS__, 'unblock' ) ); add_action( 'admin_post_rabbitproof_mode', array( __CLASS__, 'save_mode' ) ); add_action( 'admin_post_rabbitproof_checks', array( __CLASS__, 'save_checks' ) ); add_action( 'admin_post_rabbitproof_allow_ip', array( __CLASS__, 'save_allow_ip' ) ); add_action( 'admin_post_rabbitproof_visitor', array( __CLASS__, 'save_visitor' ) ); add_action( 'admin_post_rabbitproof_always', array( __CLASS__, 'save_always' ) ); add_action( 'admin_post_rabbitproof_geo', array( __CLASS__, 'save_geo' ) ); add_action( 'admin_init', array( __CLASS__, 'admin_pass' ) ); add_action( 'admin_post_rabbitproof_lock', array( __CLASS__, 'save_lock' ) ); add_action( 'wp_ajax_rabbitproof_pages', array( __CLASS__, 'pages' ) ); add_filter( 'plugin_action_links_' . plugin_basename( RABBITPROOF_FILE ), array( __CLASS__, 'links' ) ); add_action( 'admin_init', array( __CLASS__, 'privacy' ) ); } public static function privacy() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $text = '

' . __( "This website uses Rabbitproof (rabbitproof.com.au) to protect it from bots and spam. When you visit, Rabbitproof checks your browser and device and the internet address you connect from, to tell people and bots apart. It sets a cookie so you are not checked again on every page. This information is used only to protect this website and other websites that use Rabbitproof, and is kept for a short time. Rabbitproof's privacy policy: rabbitproof.com.au/privacy.", 'rabbitproof' ) . '

'; wp_add_privacy_policy_content( 'Rabbitproof', wp_kses_post( $text ) ); } public static function menu() { add_menu_page( 'Rabbitproof', 'Rabbitproof', self::CAP, self::SLUG, array( __CLASS__, 'page' ), 'dashicons-shield', 81 ); } public static function links( $links ) { array_unshift( $links, '' . esc_html__( 'Settings', 'rabbitproof' ) . '' ); return $links; } public static function assets( $hook ) { if ( 'toplevel_page_' . self::SLUG !== $hook ) { return; } $css = RABBITPROOF_VERSION . '.' . substr( (string) @md5_file( RABBITPROOF_DIR . 'assets/admin.css' ), 0, 10 ); $js = RABBITPROOF_VERSION . '.' . substr( (string) @md5_file( RABBITPROOF_DIR . 'assets/admin.js' ), 0, 10 ); wp_enqueue_style( 'rabbitproof-admin', plugins_url( 'assets/admin.css', RABBITPROOF_FILE ), array(), $css ); wp_enqueue_script( 'rabbitproof-admin', plugins_url( 'assets/admin.js', RABBITPROOF_FILE ), array(), $js, array( 'in_footer' => true ) ); } private static function guard() { if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'rabbitproof' ), 403 ); } } private static function back( $tab, $note, array $args = array() ) { if ( ! empty( $_POST['rp_inline'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in the handler (check_admin_referer). $n = self::note( $note ); wp_send_json( array( 'ok' => $n && 'err' !== $n[0], 'note' => $note, 'say' => $n ? $n[1] : '' ) ); } wp_safe_redirect( add_query_arg( array_merge( array( 'page' => self::SLUG, 'tab' => $tab ), $args, array( 'rp' => $note ) ), admin_url( 'admin.php' ) ) ); exit; } public static function connect() { self::guard(); check_admin_referer( 'rabbitproof_connect' ); $state = wp_generate_password( 40, false, false ); set_transient( self::CONNECT . md5( $state ), 1, DAY_IN_SECONDS ); $to = add_query_arg( array( 'site' => rawurlencode( home_url( '/' ) ), 'return' => rawurlencode( add_query_arg( '_wpnonce', wp_create_nonce( 'rabbitproof_connected' ), admin_url( 'admin-post.php?action=rabbitproof_connected' ) ) ), 'state' => rawurlencode( $state ), ), Rabbitproof_Api::BASE . '/connect' ); wp_redirect( $to ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Rabbitproof's own address, fixed. exit; } public static function connected() { self::guard(); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; $sent = false !== wp_verify_nonce( $nonce, 'rabbitproof_connected' ); $state = $sent && isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; $code = $sent && isset( $_GET['code'] ) ? sanitize_text_field( wp_unslash( $_GET['code'] ) ) : ''; $key = self::CONNECT . md5( $state ); $known = $sent && '' !== $state && 1 === preg_match( '/^[A-Za-z0-9]{24,64}$/', $state ) && false !== get_transient( $key ); if ( '' !== $state ) { delete_transient( $key ); } if ( ! $known ) { self::back( 'overview', Rabbitproof_Options::connected() && ! empty( Rabbitproof_Options::all()['site']['id'] ) ? '' : 'connect_again' ); } if ( isset( $_GET['error'] ) || '' === $code ) { self::back( 'overview', 'connect_cancelled' ); } $host = (string) wp_parse_url( admin_url( '/' ), PHP_URL_HOST ); $answer = Rabbitproof_Api::open( '/v1/connect/exchange', array( 'code' => $code, 'state' => $state, 'host' => strtolower( $host ) ) ); if ( is_wp_error( $answer ) || 0 === strpos( (string) $answer['_status'], '5' ) ) { self::back( 'overview', 'down' ); } $site_key = isset( $answer['site_key'] ) ? (string) $answer['site_key'] : ''; $secret_key = isset( $answer['secret_key'] ) ? (string) $answer['secret_key'] : ''; if ( 200 !== $answer['_status'] || ! Rabbitproof_Options::valid_site_key( $site_key ) || ! Rabbitproof_Options::valid_secret_key( $secret_key ) ) { self::back( 'overview', 'connect_again' ); } self::use_keys( $site_key, $secret_key ); } public static function connected_signin() { $back = array( 'action' => 'rabbitproof_connected' ); foreach ( array( '_wpnonce', 'state', 'code', 'error' ) as $k ) { if ( isset( $_GET[ $k ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only carried to the sign-in page; connected() checks them. $back[ $k ] = rawurlencode( sanitize_text_field( wp_unslash( $_GET[ $k ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended } } wp_safe_redirect( wp_login_url( add_query_arg( $back, admin_url( 'admin-post.php' ) ) ) ); exit; } public static function remove_leftover() { self::guard(); check_admin_referer( 'rabbitproof_cleanup' ); $o = Rabbitproof_Options::all(); if ( ! isset( $o['site']['tier'] ) || 'advanced' !== $o['site']['tier'] ) { self::back( 'overview', 'saved' ); } self::back( 'overview', Rabbitproof_Cleanup::remove() ? 'cleanup_removed' : 'cleanup_kept' ); } private static function use_keys( $site_key, $secret_key ) { $answer = Rabbitproof_Api::call( 'GET', '/v1/sites', null, 10, $secret_key ); if ( is_wp_error( $answer ) || 0 === strpos( (string) $answer['_status'], '5' ) ) { self::back( 'keys', 'down' ); } $site = isset( $answer['sites'][0] ) && is_array( $answer['sites'][0] ) ? $answer['sites'][0] : null; if ( 200 !== $answer['_status'] || ! $site ) { self::back( 'keys', 'wrongkey' ); } if ( ! isset( $site['site_key'] ) || $site['site_key'] !== $site_key ) { self::back( 'keys', 'mismatch' ); } $proof = isset( $site['verify']['file']['contents'] ) ? (string) $site['verify']['file']['contents'] : ''; delete_option( 'rabbitproof_gone' ); Rabbitproof_Options::update( array( 'site_key' => $site_key, 'secret_key' => $secret_key, 'site' => self::summary( $site ), 'proof' => $proof, ) ); delete_transient( self::WEEK ); if ( ! empty( $site['verified'] ) ) { wp_schedule_single_event( time(), Rabbitproof_Map::HOOK ); self::back( 'overview', 'connected_button' ); } if ( self::verify_now( $site['id'] ) ) { self::back( 'overview', 'connected_button' ); } self::back( 'keys', self::proof_note() ); } private static function verify_now( $id ) { $verify = Rabbitproof_Api::call( 'POST', '/v1/sites/' . rawurlencode( $id ) . '/verify', array( 'method' => 'file' ), 20 ); if ( is_wp_error( $verify ) || 200 !== $verify['_status'] || empty( $verify['site']['verified'] ) ) { return false; } Rabbitproof_Options::update( array( 'site' => self::summary( $verify['site'] ) ) ); wp_schedule_single_event( time(), Rabbitproof_Map::HOOK ); return true; } private static function proof_note() { return file_exists( ABSPATH . '.well-known/rabbitproof-verify.txt' ) ? 'oldproof' : 'noproof'; } public static function verify_again() { self::guard(); check_admin_referer( 'rabbitproof_verify' ); $o = Rabbitproof_Options::all(); if ( empty( $o['site']['id'] ) || ! Rabbitproof_Options::connected() ) { self::back( 'overview', 'connect_again' ); } self::back( 'overview', self::verify_now( $o['site']['id'] ) ? 'verified' : self::proof_note() ); } private static function summary( array $site ) { return array( 'id' => isset( $site['id'] ) ? (string) $site['id'] : '', 'website' => isset( $site['website'] ) ? (string) $site['website'] : '', 'tier' => isset( $site['tier'] ) ? (string) $site['tier'] : 'none', 'mode' => isset( $site['options']['mode'] ) ? (string) $site['options']['mode'] : 'watch', 'verified' => ! empty( $site['verified'] ), 'first_left' => ! isset( $site['first_left'] ) || ! empty( $site['first_left'] ), 'extras' => ! empty( $site['extras'] ), 'wordpress' => ! empty( $site['wordpress'] ), 'bm' => self::bm_of( $site ), 'checks' => self::checks_of( $site ), 'ips' => self::ips_of( $site ), 'pass_days' => isset( $site['options']['pass_days'] ) && in_array( (int) $site['options']['pass_days'], self::PASS_DAYS, true ) ? (int) $site['options']['pass_days'] : 7, 'check_theme' => isset( $site['options']['check_theme'] ) && in_array( $site['options']['check_theme'], self::THEMES, true ) ? $site['options']['check_theme'] : 'auto', 'checked' => time(), ); } const BOTS = array( 'search' => array( 'Search engines', 'Google, Bing, DuckDuckGo, Yandex, Baidu.' ), 'preview' => array( 'Link previews', 'The picture and title shown when your link is shared on Facebook, X, LinkedIn, Slack or Discord.' ), 'ai_search' => array( 'AI search', 'Search run by AI services, which can send visitors to you.' ), 'ai_assistant' => array( 'AI assistants', 'An AI opening your page because a person asked it to, such as Perplexity.' ), 'ai_crawler' => array( 'AI training crawlers', 'Bots that copy pages to train AI models: OpenAI, Anthropic, Amazon and others.' ), 'ads' => array( 'Advertising checks', 'Google Ads and others checking the page an ad points to. Off can get ads disapproved.' ), 'monitoring' => array( 'Uptime monitors', 'Services that check your website is up, such as UptimeRobot.' ), 'feeds' => array( 'Feed readers', 'Apps that fetch your RSS feed or podcast.' ), 'accessibility' => array( 'Accessibility checkers', 'Tools that check your website for people with a disability.' ), 'seo' => array( 'SEO tools', 'Ahrefs, Semrush, Lighthouse and others that read your pages to report on them.' ), 'social' => array( 'Social media tools', 'Services that post and track for businesses on social media.' ), 'aggregators' => array( 'Content aggregators', 'Services that collect pages from many websites into one place.' ), 'archives' => array( 'Web archives', 'Services that keep old copies of websites, such as the Wayback Machine.' ), 'academic' => array( 'Academic research', 'Universities collecting pages for research.' ), ); const BOT_DEFAULT = array( 'search', 'preview', 'ai_search', 'ai_assistant', 'ads', 'monitoring', 'webhooks', 'feeds', 'accessibility' ); const CHECKS = array( 'disguise', 'many_places', 'repeat', 'hidden_location', 'known' ); const PASS_DAYS = array( 1, 7, 30 ); const THEMES = array( 'light', 'dark', 'auto' ); const GEO_MAX = 100; public static function product( array $site ) { $tier = isset( $site['tier'] ) ? $site['tier'] : 'none'; if ( 'plugin' === $tier ) { return array( __( 'WordPress Plugin', 'rabbitproof' ), 'A$5', '25,000' ); } if ( 'advanced' === $tier ) { return array( __( 'Bot Management for WordPress', 'rabbitproof' ), 'A$39', '1,000,000' ); } if ( 'standard' === $tier ) { return array( __( 'Bot and Spam Protection', 'rabbitproof' ), 'A$14', '100,000' ); } return null; } private static function checks_of( array $site ) { $checks = array(); foreach ( self::CHECKS as $k ) { $checks[ $k ] = ! isset( $site['options']['checks'][ $k ] ) || ! empty( $site['options']['checks'][ $k ] ); } return $checks; } private static function bm_of( array $site ) { $o = isset( $site['options'] ) && is_array( $site['options'] ) ? $site['options'] : array(); $checks = self::checks_of( $site ); $allow = isset( $o['bots']['allow'] ) && is_array( $o['bots']['allow'] ) ? array_values( array_intersect( array_merge( array_keys( self::BOTS ), array( 'webhooks' ) ), $o['bots']['allow'] ) ) : self::BOT_DEFAULT; $paths = function ( $v ) { return is_array( $v ) ? array_values( array_filter( array_map( 'strval', $v ), function ( $p ) { return '' !== $p && '/' === $p[0]; } ) ) : array(); }; return array( 'checks' => $checks, 'bots' => $allow, 'listings' => ! empty( $o['extras']['page_numbers']['on'] ), 'trap' => ! empty( $o['extras']['trap']['on'] ), 'always' => $paths( isset( $o['forms']['always'] ) ? $o['forms']['always'] : array() ), 'found' => $paths( isset( $site['found']['always'] ) ? $site['found']['always'] : array() ), 'out' => $paths( isset( $o['forms']['out'] ) ? $o['forms']['out'] : array() ), 'geoblock' => array_values( array_filter( isset( $o['geoblock'] ) && is_array( $o['geoblock'] ) ? $o['geoblock'] : array(), array( __CLASS__, 'is_place' ) ) ), ); } private static function ips_of( array $site ) { $out = array(); $ips = isset( $site['options']['allow']['ips'] ) && is_array( $site['options']['allow']['ips'] ) ? $site['options']['allow']['ips'] : array(); foreach ( $ips as $e ) { if ( is_array( $e ) && isset( $e['ip'] ) && is_string( $e['ip'] ) ) { $out[] = array( 'ip' => $e['ip'], 'note' => isset( $e['note'] ) ? (string) $e['note'] : '' ); } } return array_slice( $out, 0, 50 ); } public static function save_allow_ip() { self::guard(); check_admin_referer( 'rabbitproof_allow_ip' ); $add = isset( $_POST['add'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['add'] ) ) ) : ''; $note = isset( $_POST['note'] ) ? substr( trim( sanitize_text_field( wp_unslash( $_POST['note'] ) ) ), 0, 40 ) : ''; $rm = isset( $_POST['remove'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['remove'] ) ) ) : ''; if ( '' !== $add && false === filter_var( $add, FILTER_VALIDATE_IP ) ) { self::back( 'allow', 'badip' ); } self::change_options( function ( $o ) use ( $add, $note, $rm ) { $list = isset( $o['allow']['ips'] ) && is_array( $o['allow']['ips'] ) ? $o['allow']['ips'] : array(); if ( '' !== $rm ) { $list = array_values( array_filter( $list, function ( $e ) use ( $rm ) { return ! is_array( $e ) || ! isset( $e['ip'] ) || $e['ip'] !== $rm; } ) ); } if ( '' !== $add ) { $list[] = array( 'ip' => $add, 'note' => $note ); } $o['allow'] = array( 'ips' => array_values( $list ) ); return $o; }, 'allow', 'badip' ); } public static function save_visitor() { self::guard(); check_admin_referer( 'rabbitproof_visitor' ); $o = Rabbitproof_Options::all(); $ref = isset( $_POST['ref'] ) ? sanitize_key( wp_unslash( $_POST['ref'] ) ) : ''; $do = isset( $_POST['do'] ) && 'keep' === sanitize_key( wp_unslash( $_POST['do'] ) ) ? 'keep' : 'remove'; if ( empty( $o['site']['id'] ) || 1 !== preg_match( '/^[0-9a-f]{6}$/', $ref ) ) { self::back( 'allow', 'down' ); } $a = Rabbitproof_Api::call( 'POST', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/allowlist/' . $ref . '/' . $do, array(), 10 ); self::back( 'allow', ! is_wp_error( $a ) && 200 === $a['_status'] ? ( 'keep' === $do ? 'kept' : 'removed' ) : 'down' ); } private static function change_options( $change, $tab, $bad = 'badpath' ) { $o = Rabbitproof_Options::all(); if ( empty( $o['site']['id'] ) ) { self::back( $tab, 'down' ); } $id = rawurlencode( $o['site']['id'] ); $fresh = Rabbitproof_Api::call( 'GET', '/v1/sites/' . $id, null, 10 ); if ( is_wp_error( $fresh ) || 200 !== $fresh['_status'] || ! isset( $fresh['site']['version'] ) ) { self::back( $tab, 'down' ); } $options = call_user_func( $change, isset( $fresh['site']['options'] ) && is_array( $fresh['site']['options'] ) ? $fresh['site']['options'] : array() ); if ( null === $options ) { self::back( $tab, $bad ); } $a = Rabbitproof_Api::call( 'PATCH', '/v1/sites/' . $id . '/options', array( 'options' => $options, 'version' => (int) $fresh['site']['version'] ), 10 ); if ( is_wp_error( $a ) || 200 !== $a['_status'] || empty( $a['site'] ) ) { self::back( $tab, 'down' ); } Rabbitproof_Options::update( array( 'site' => self::summary( $a['site'] ) ) ); delete_transient( self::WEEK ); self::back( $tab, 'saved' ); } public static function admin_pass() { if ( ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) || ! current_user_can( 'manage_options' ) || ! Rabbitproof_Options::connected() ) { return; } $o = Rabbitproof_Options::all(); if ( empty( $o['site']['id'] ) || ! isset( $o['site']['tier'] ) || 'advanced' !== $o['site']['tier'] ) { return; } $have = isset( $_COOKIE['rp_admin'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['rp_admin'] ) ) : ''; if ( 1 === preg_match( '/^(\d{9,11})\.[0-9a-f]{16}\.[0-9a-f]{32}$/', $have, $m ) && time() - (int) $m[1] < HOUR_IN_SECONDS ) { return; } $a = Rabbitproof_Api::call( 'POST', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/admin-pass', array( 'ip' => Rabbitproof_Ip::visitor( $_SERVER, (bool) Rabbitproof_Options::get( 'proxy' ) ) ), 3 ); if ( is_wp_error( $a ) || 200 !== $a['_status'] || empty( $a['pass'] ) || 1 !== preg_match( '/^\d{9,11}\.[0-9a-f]{16}\.[0-9a-f]{32}$/', (string) $a['pass'] ) ) { return; } $days = isset( $a['days'] ) ? max( 1, min( 7, (int) $a['days'] ) ) : 7; if ( ! headers_sent() ) { setcookie( 'rp_admin', (string) $a['pass'], array( 'expires' => time() + $days * DAY_IN_SECONDS, 'path' => '/', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax' ) ); } $_COOKIE['rp_admin'] = (string) $a['pass']; } public static function save_checks() { self::guard(); check_admin_referer( 'rabbitproof_checks' ); $part = isset( $_POST['part'] ) ? sanitize_key( wp_unslash( $_POST['part'] ) ) : ''; $on = function ( $name ) { return ! empty( $_POST[ $name ] ); }; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in the handler (check_admin_referer). if ( 'checks' === $part ) { self::change_options( function ( $o ) use ( $on ) { foreach ( self::CHECKS as $k ) { $o['checks'][ $k ] = $on( 'check_' . $k ); } return $o; }, 'protect' ); } if ( 'days' === $part ) { $days = isset( $_POST['pass_days'] ) ? (int) $_POST['pass_days'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked above (check_admin_referer). $theme = isset( $_POST['check_theme'] ) ? sanitize_key( wp_unslash( $_POST['check_theme'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked above (check_admin_referer). if ( ! in_array( $days, self::PASS_DAYS, true ) || ( '' !== $theme && ! in_array( $theme, self::THEMES, true ) ) ) { self::back( 'protect', 'down' ); } self::change_options( function ( $o ) use ( $days, $theme ) { $o['pass_days'] = $days; if ( '' !== $theme ) { $o['check_theme'] = $theme; } return $o; }, 'protect' ); } if ( 'bots' === $part ) { self::change_options( function ( $o ) use ( $on ) { $all = isset( $o['bots']['allow'] ) && is_array( $o['bots']['allow'] ) ? $o['bots']['allow'] : self::BOT_DEFAULT; $hooks = in_array( 'webhooks', $all, true ); $was = array_values( array_diff( $all, array( 'webhooks' ) ) ); $picked = array(); foreach ( array_keys( self::BOTS ) as $k ) { if ( $on( 'bot_' . $k ) ) { $picked[] = $k; } } $allow = ! $on( 'bots_on' ) ? array() : ( empty( $was ) ? array_values( array_diff( self::BOT_DEFAULT, array( 'webhooks' ) ) ) : $picked ); if ( $hooks ) { $allow[] = 'webhooks'; } $o['checks']['search_engines'] = true; $o['bots'] = array( 'allow' => array_values( $allow ) ); return $o; }, 'allow' ); } if ( 'hooks' === $part ) { self::change_options( function ( $o ) use ( $on ) { $all = isset( $o['bots']['allow'] ) && is_array( $o['bots']['allow'] ) ? $o['bots']['allow'] : self::BOT_DEFAULT; $allow = array_values( array_diff( $all, array( 'webhooks' ) ) ); if ( $on( 'hooks_on' ) ) { $allow[] = 'webhooks'; } $o['bots'] = array( 'allow' => $allow ); return $o; }, 'allow' ); } if ( 'extras' === $part ) { self::change_options( function ( $o ) use ( $on ) { $o['extras']['page_numbers']['on'] = $on( 'listings' ); $o['extras']['trap']['on'] = $on( 'trap' ); return $o; }, 'protect' ); } self::back( 'protect', 'down' ); } public static function save_lock() { self::guard(); check_admin_referer( 'rabbitproof_lock' ); $do = isset( $_POST['do'] ) ? sanitize_key( wp_unslash( $_POST['do'] ) ) : ''; if ( 'unlock' === $do ) { self::back( 'protect', Rabbitproof_Lock::unlock() ? 'unlocked' : 'lock_nofile' ); } self::back( 'protect', 'lock_' . Rabbitproof_Lock::lock() ); } public static function save_always() { self::guard(); check_admin_referer( 'rabbitproof_always' ); $add = isset( $_POST['add'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['add'] ) ) ) : ''; $rm = isset( $_POST['remove'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['remove'] ) ) ) : ''; $saved = Rabbitproof_Options::all(); $found = isset( $saved['site']['bm']['found'] ) && is_array( $saved['site']['bm']['found'] ) ? $saved['site']['bm']['found'] : array(); self::change_options( function ( $o ) use ( $add, $rm, $found ) { $list = isset( $o['forms']['always'] ) && is_array( $o['forms']['always'] ) ? $o['forms']['always'] : array(); $out = isset( $o['forms']['out'] ) && is_array( $o['forms']['out'] ) ? $o['forms']['out'] : array(); if ( '' !== $add ) { if ( ! preg_match( '#^/[^\s]{0,199}$#', $add ) ) { return null; } if ( ! in_array( $add, $list, true ) ) { $list[] = $add; } $out = array_values( array_diff( $out, array( $add ) ) ); } if ( '' !== $rm ) { $list = array_values( array_diff( $list, array( $rm ) ) ); if ( in_array( $rm, $found, true ) && ! in_array( $rm, $out, true ) ) { $out[] = $rm; } } $o['forms']['always'] = $list; $o['forms']['out'] = $out; return $o; }, 'allow' ); } public static function save_geo() { self::guard(); check_admin_referer( 'rabbitproof_geo' ); $add = isset( $_POST['add'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['add'] ) ) ) : ''; $rm = isset( $_POST['remove'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['remove'] ) ) ) : ''; $off = isset( $_POST['part'] ) && 'switch' === sanitize_key( wp_unslash( $_POST['part'] ) ) && empty( $_POST['geo_on'] ); if ( isset( $_POST['part'] ) && 'switch' === sanitize_key( wp_unslash( $_POST['part'] ) ) && ! $off ) { self::back( 'protect', 'geo_open' ); } $add = '' === $add ? '' : self::place_of( $add ); self::change_options( function ( $o ) use ( $add, $rm, $off ) { $list = isset( $o['geoblock'] ) && is_array( $o['geoblock'] ) ? $o['geoblock'] : array(); if ( $off ) { $list = array(); } if ( null === $add ) { return null; } if ( '' !== $add && ! in_array( $add, $list, true ) && count( $list ) < self::GEO_MAX ) { $list[] = $add; } if ( '' !== $rm ) { $list = array_values( array_diff( $list, array( $rm ) ) ); } $o['geoblock'] = array_values( $list ); return $o; }, 'protect', 'badplace' ); } public static function is_place( $x ) { if ( ! is_string( $x ) ) { return false; } foreach ( Rabbitproof_Regions::REGIONS as $r ) { if ( $r[0] === $x ) { return true; } } return isset( Rabbitproof_Regions::COUNTRIES[ $x ] ); } private static function place_of( $v ) { if ( self::is_place( $v ) ) { return $v; } $low = function_exists( 'mb_strtolower' ) ? mb_strtolower( $v ) : strtolower( $v ); $eq = function ( $name ) use ( $low ) { return ( function_exists( 'mb_strtolower' ) ? mb_strtolower( $name ) : strtolower( $name ) ) === $low; }; foreach ( Rabbitproof_Regions::REGIONS as $r ) { if ( $eq( $r[1] ) ) { return $r[0]; } } foreach ( Rabbitproof_Regions::COUNTRIES as $c => $name ) { if ( $eq( $name ) ) { return $c; } } return null; } public static function place_name( $x ) { foreach ( Rabbitproof_Regions::REGIONS as $r ) { if ( $r[0] === $x ) { /* translators: %d: how many countries the region holds */ return array( $r[1], sprintf( __( 'Region, %d countries', 'rabbitproof' ), $r[2] ) ); } } return array( isset( Rabbitproof_Regions::COUNTRIES[ $x ] ) ? Rabbitproof_Regions::COUNTRIES[ $x ] : $x, '' ); } private static function built_in( $path ) { if ( '/wc-api/' === $path || '/?wc-api' === $path ) { return __( 'Payment notices from WooCommerce. Added for you.', 'rabbitproof' ); } return '/wp-cron.php' === $path ? __( "WordPress's own scheduler. Added for you.", 'rabbitproof' ) : ''; } private static function wp_own( $path ) { $bare = strtok( $path, '?' ); return 0 === strpos( $path, '/wp-admin/' ) || in_array( $bare, array( '/wp-comments-post.php', '/wp-login.php', '/xmlrpc.php' ), true ); } private static function always_rows( array $bm, array $site ) { $posts = array(); $from = array(); $p = empty( $site['id'] ) ? null : Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $site['id'] ) . '/posts?days=7', null, 5 ); if ( is_array( $p ) && 200 === $p['_status'] && isset( $p['posts'] ) && is_array( $p['posts'] ) ) { foreach ( $p['posts'] as $x ) { if ( is_array( $x ) && isset( $x['path'] ) && is_string( $x['path'] ) && '' !== $x['path'] ) { $posts[ $x['path'] ] = isset( $x['n'] ) ? max( 1, (int) $x['n'] ) : 1; $from[ $x['path'] ] = isset( $x['sender'] ) && is_string( $x['sender'] ) && in_array( $x['sender'], Rabbitproof_Guard::SENDERS, true ) ? $x['sender'] : ''; } } } $bm['out'] = isset( $bm['out'] ) && is_array( $bm['out'] ) ? $bm['out'] : array(); $allowed = array(); foreach ( array_unique( array_merge( $bm['always'], $bm['found'] ) ) as $a ) { if ( in_array( $a, $bm['out'], true ) || ( '' !== self::built_in( $a ) && '/wp-cron.php' !== $a && ! class_exists( 'WooCommerce' ) ) ) { continue; } $note = isset( $posts[ $a ] ) ? sprintf( /* translators: %s: a number */ _n( '%s sent this week. All went through.', '%s sent this week. All went through.', $posts[ $a ], 'rabbitproof' ), number_format_i18n( $posts[ $a ] ) ) : ( '' !== self::built_in( $a ) ? self::built_in( $a ) : ( in_array( $a, $bm['found'], true ) ? __( 'Found on your website. Added for you.', 'rabbitproof' ) : __( 'Added by you.', 'rabbitproof' ) ) ); $allowed[] = array( 'path' => $a, 'note' => $note ); } $waiting = array(); foreach ( $posts as $path => $n ) { if ( '' === $from[ $path ] || in_array( $path, array_column( $allowed, 'path' ), true ) || self::wp_own( $path ) ) { continue; } /* translators: 1: a service, such as Stripe 2: a number */ $waiting[] = array( 'path' => $path, 'note' => sprintf( _n( 'From %1$s. %2$s sent this week.', 'From %1$s. %2$s sent this week.', $n, 'rabbitproof' ), $from[ $path ], number_format_i18n( $n ) ) ); } return array( 'allowed' => $allowed, 'waiting' => $waiting ); } const OUTCOMES = array( 'all' => 'All', 'blocked' => 'Blocked', 'would' => 'Would block', 'allowed' => 'Allowed', 'unblocked' => 'Unblocked', ); public static function outcomes() { return array( 'all' => __( 'All', 'rabbitproof' ), 'blocked' => __( 'Blocked', 'rabbitproof' ), 'would' => __( 'Would block', 'rabbitproof' ), 'allowed' => __( 'Allowed', 'rabbitproof' ), 'unblocked' => __( 'Unblocked', 'rabbitproof' ), ); } public static function outcome( array $e ) { $oc = isset( $e['outcome'] ) ? (string) $e['outcome'] : ''; if ( in_array( $oc, array( 'allowed', 'blocked', 'would', 'unblocked' ), true ) ) { return $oc; } return ! empty( $e['blocked'] ) ? 'blocked' : 'would'; } public static function flag( $cc ) { $cc = strtolower( (string) $cc ); if ( 1 !== preg_match( '/^[a-z]{2}$/', $cc ) || ! file_exists( RABBITPROOF_DIR . 'assets/flags/' . $cc . '.svg' ) ) { return ''; } return ''; } public static function when( $t ) { $day = wp_date( 'Y-m-d', $t ); if ( $day === wp_date( 'Y-m-d' ) ) { return wp_date( 'H:i', $t ); } if ( $day === wp_date( 'Y-m-d', time() - 86400 ) ) { /* translators: %s: a time, such as 10:30 */ return sprintf( __( 'Yesterday %s', 'rabbitproof' ), wp_date( 'H:i', $t ) ); } return wp_date( 'j M H:i', $t ); } public static function form_label( $form ) { $names = array( 'cf7' => 'Contact Form 7', 'wpforms' => 'WPForms', 'gravity' => 'Gravity Forms', 'fluent' => 'Fluent Forms', 'elementor' => 'Elementor', 'comments' => __( 'Comment', 'rabbitproof' ), 'login' => __( 'Sign-in', 'rabbitproof' ), 'signups' => __( 'Sign-up', 'rabbitproof' ), 'woocommerce' => 'WooCommerce', ); return isset( $names[ $form ] ) ? $names[ $form ] : ''; } public static function spam_link( $form ) { switch ( $form ) { case 'comments': return admin_url( 'edit-comments.php?comment_status=spam' ); case 'cf7': return class_exists( 'Flamingo_Inbound_Message' ) ? admin_url( 'admin.php?page=flamingo_inbound&post_status=spam' ) : ''; case 'fluent': return defined( 'FLUENTFORM' ) || function_exists( 'wpFluentForm' ) ? admin_url( 'admin.php?page=fluent_forms_all_entries' ) : ''; case 'gravity': return class_exists( 'GFForms' ) ? admin_url( 'admin.php?page=gf_entries' ) : ''; } return ''; } public static function form_note( $form ) { switch ( $form ) { case 'cf7': return class_exists( 'Flamingo_Inbound_Message' ) ? __( 'Sent through Contact Form 7. You can read it in Flamingo, under Inbound Messages.', 'rabbitproof' ) : __( 'Sent through Contact Form 7, which emails it to you and keeps no copy.', 'rabbitproof' ); case 'wpforms': $pro = function_exists( 'wpforms' ) && is_object( wpforms() ) && method_exists( wpforms(), 'is_pro' ) && wpforms()->is_pro(); return $pro ? __( 'Sent through WPForms. You can read it in WPForms, under Entries.', 'rabbitproof' ) : __( 'Sent through WPForms, which emails it to you and keeps no copy.', 'rabbitproof' ); case 'gravity': return __( 'Sent through Gravity Forms. You can read it in Gravity Forms, under Entries.', 'rabbitproof' ); case 'fluent': return __( 'Sent through Fluent Forms. You can read it in Fluent Forms, under Entries.', 'rabbitproof' ); case 'elementor': return __( 'Sent through an Elementor form. If Submissions is switched on in Elementor, you can read it there.', 'rabbitproof' ); case 'comments': return __( 'A comment. You can read it in Comments.', 'rabbitproof' ); } return ''; } private static function comments_of( array $rows ) { $refs = array(); foreach ( $rows as $e ) { if ( isset( $e['form'], $e['ref'] ) && 'comments' === $e['form'] && ! empty( $e['blocked'] ) && '' !== $e['ref'] ) { $refs[ $e['ref'] ] = true; } } if ( empty( $refs ) ) { return array(); } $found = get_comments( array( 'meta_key' => 'rabbitproof_ref', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one page of rows, the administrator only. 'meta_value' => array_keys( $refs ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'meta_compare' => 'IN', 'status' => array( 'spam', 'hold', 'approve' ), 'orderby' => 'comment_date_gmt', 'order' => 'DESC', 'number' => 100, ) ); $out = array(); foreach ( is_array( $found ) ? $found : array() as $c ) { $ref = (string) get_comment_meta( (int) $c->comment_ID, 'rabbitproof_ref', true ); if ( isset( $out[ $ref ] ) || ! isset( $refs[ $ref ] ) ) { continue; } $out[ $ref ] = array( 'text' => mb_substr( trim( wp_strip_all_tags( (string) $c->comment_content ) ), 0, 5000 ), 'who' => sanitize_text_field( (string) $c->comment_author ), 'mail' => sanitize_text_field( (string) $c->comment_author_email ), 'url' => sanitize_text_field( (string) $c->comment_author_url ), 'post' => (int) $c->comment_post_ID > 0 ? wp_strip_all_tags( get_the_title( (int) $c->comment_post_ID ) ) : '', 'spam' => 'spam' === (string) $c->comment_approved, ); } return $out; } public static function comment_on( array $e, array $comments ) { if ( empty( $e['ref'] ) || 'comments' !== $e['form'] || empty( $e['blocked'] ) || ! isset( $comments[ $e['ref'] ] ) || '' === $comments[ $e['ref'] ]['post'] ) { return ''; } /* translators: %s: the post's title */ return sprintf( __( 'On %s', 'rabbitproof' ), $comments[ $e['ref'] ]['post'] ); } private static function row_of( array $e ) { return array( 't' => isset( $e['t'] ) ? (int) $e['t'] : 0, 'n' => isset( $e['n'] ) ? max( 1, (int) $e['n'] ) : 1, 'kind' => isset( $e['kind'] ) ? (string) $e['kind'] : '', 'why' => isset( $e['why'] ) ? (string) $e['why'] : '', 'about' => isset( $e['about'] ) ? (string) $e['about'] : '', 'form' => isset( $e['form'] ) && 1 === preg_match( '/^[a-z0-9]{1,20}$/', (string) $e['form'] ) ? (string) $e['form'] : '', 'path' => isset( $e['path'] ) && '' !== $e['path'] ? (string) $e['path'] : '/', 'cc' => isset( $e['cc'] ) ? (string) $e['cc'] : '', 'ref' => isset( $e['ref'] ) && 1 === preg_match( '/^[0-9a-f]{6}$/', (string) $e['ref'] ) ? (string) $e['ref'] : '', 'outcome' => isset( $e['outcome'] ) ? (string) $e['outcome'] : '', 'blocked' => ! empty( $e['blocked'] ), 'id' => isset( $e['id'] ) ? (int) $e['id'] : 0, 'again' => ! empty( $e['can_unlet'] ) && ! empty( $e['id'] ), 'let' => ! empty( $e['can_let'] ) && ! empty( $e['id'] ), 'addrs' => isset( $e['addrs'] ) ? max( 0, (int) $e['addrs'] ) : 0, 'first' => isset( $e['first'] ) ? (int) $e['first'] : 0, ); } public static function bursts( array $rows ) { $out = array(); foreach ( $rows as $r ) { $i = count( $out ) - 1; if ( $i >= 0 && self::scan_row( $out[ $i ] ) && self::scan_row( $r ) && ( ! empty( $out[ $i ]['first'] ) ? $out[ $i ]['first'] : $out[ $i ]['t'] ) - $r['t'] <= Rabbitproof_Probes::SCAN_GAP ) { $p = $out[ $i ]; if ( empty( $p['burst'] ) ) { $p = self::as_burst( $p ); } $q = self::as_burst( $r ); $p['n'] += $q['n']; $p['first'] = min( $p['first'], $q['first'] ); $p['tried'] = array_slice( array_merge( $p['tried'], $q['tried'] ), 0, 6 ); $p['mixed'] = $p['mixed'] || $q['mixed'] || $p['local'] !== $q['local']; $p['addrs'] = $p['mixed'] ? 0 : $p['addrs'] + $q['addrs']; $p['id'] = $p['mixed'] ? 0 : $p['id']; $out[ $i ] = $p; continue; } $out[] = ( ! empty( $r['addrs'] ) && $r['addrs'] > 1 ) ? self::as_burst( $r ) : $r; } return $out; } public static function burst_line( array $e ) { /* translators: 1: a time like 20:12, 2: a time like 20:33 */ $span = sprintf( __( '%1$s to %2$s', 'rabbitproof' ), wp_date( 'H:i', (int) $e['first'] ), wp_date( 'H:i', (int) $e['t'] ) ); if ( ! empty( $e['addrs'] ) && $e['addrs'] > 1 ) { /* translators: 1: a number of requests, 2: a number of addresses, 3: from when to when */ return sprintf( __( '%1$s requests from %2$s addresses, %3$s', 'rabbitproof' ), number_format_i18n( $e['n'] ), number_format_i18n( $e['addrs'] ), $span ); } /* translators: 1: a number of requests, 2: from when to when */ return sprintf( __( '%1$s requests, %2$s', 'rabbitproof' ), number_format_i18n( $e['n'] ), $span ); } private static function scan_row( array $r ) { return 'Vulnerability scan' === $r['why'] && ! empty( $r['blocked'] ) && 'visit' === $r['kind']; } private static function as_burst( array $r ) { $r['burst'] = true; $r['local'] = ! empty( $r['local'] ); $r['mixed'] = ! empty( $r['mixed'] ); $r['addrs'] = ! empty( $r['addrs'] ) ? (int) $r['addrs'] : 1; $r['first'] = ! empty( $r['first'] ) ? (int) $r['first'] : (int) $r['t']; $r['tried'] = isset( $r['tried'] ) && is_array( $r['tried'] ) ? $r['tried'] : array( array( 't' => (int) $r['t'], 'path' => (string) $r['path'] ) ); $r['ref'] = ''; $r['let'] = false; $r['again'] = false; return $r; } public static function pages() { check_ajax_referer( 'rabbitproof_pages' ); if ( ! current_user_can( self::CAP ) ) { wp_send_json_error( null, 403 ); } $row = isset( $_GET['row'] ) ? absint( wp_unslash( $_GET['row'] ) ) : 0; $site = Rabbitproof_Options::all()['site']; $id = is_array( $site ) && ! empty( $site['id'] ) ? (string) $site['id'] : ''; if ( 0 === $row || '' === $id ) { wp_send_json_error( null, 400 ); } $from = isset( $_GET['from'] ) ? absint( wp_unslash( $_GET['from'] ) ) : 0; $a = Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $id ) . '/log/' . $row . '/pages' . ( $from > 0 ? '?from=' . $from : '' ), null, 10 ); if ( is_wp_error( $a ) || 200 !== $a['_status'] ) { wp_send_json_error( null, 502 ); } $out = array(); foreach ( isset( $a['pages'] ) && is_array( $a['pages'] ) ? $a['pages'] : array() as $p ) { $out[] = array( 't' => wp_date( 'H:i:s', isset( $p['t'] ) ? (int) $p['t'] : 0 ), 'path' => isset( $p['path'] ) && '' !== $p['path'] ? (string) $p['path'] : '/', 'ref' => isset( $p['ref'] ) && 1 === preg_match( '/^[0-9a-f]{6}$/', (string) $p['ref'] ) ? (string) $p['ref'] : '', ); } wp_send_json_success( array( 'pages' => $out ) ); } public static function read_search( $s ) { $s = strtolower( trim( (string) $s ) ); if ( '' === $s ) { return array( 'none' => true ); } if ( 1 === preg_match( '/^[0-9a-f]{6}$/', $s ) ) { return array( 'ref' => $s ); } if ( 1 === preg_match( '/^(\d{1,2})(?:[:.](\d{2}))?\s*(am|pm)?$/', $s, $t ) && ( ( isset( $t[2] ) && '' !== $t[2] ) || ! empty( $t[3] ) ) ) { $h = (int) $t[1]; $m = isset( $t[2] ) ? (int) $t[2] : 0; $ap = isset( $t[3] ) ? $t[3] : ''; if ( 'pm' === $ap && $h < 12 ) { $h += 12; } if ( 'am' === $ap && 12 === $h ) { $h = 0; } if ( $h < 24 && $m < 60 ) { return array( 'time' => array( $h, $m ) ); } } if ( 1 === preg_match( '#^/[a-z0-9._~/%+-]{0,159}$#', $s ) ) { return array( 'path' => $s ); } if ( 1 === preg_match( "/^(?=.*[a-z])[a-z0-9 .'&-]{2,60}$/", $s ) ) { return array( 'word' => $s ); } return array( 'bad' => true ); } public static function search_arg( array $w ) { if ( isset( $w['ref'] ) ) { return $w['ref']; } if ( isset( $w['time'] ) ) { return sprintf( '%02d:%02d', $w['time'][0], $w['time'][1] ); } if ( isset( $w['word'] ) ) { return $w['word']; } return isset( $w['path'] ) ? $w['path'] : ''; } private static function at_time( array $hm ) { $tz = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( 'UTC' ); $d = new DateTime( 'now', $tz ); $d->setTime( (int) $hm[0], (int) $hm[1], 0 ); if ( $d->getTimestamp() > time() ) { $d->modify( '-1 day' ); } return $d->getTimestamp(); } private static function found_in( array $rows, array $w, $target ) { if ( ! $rows || ( ! isset( $w['ref'] ) && ! isset( $w['time'] ) ) ) { return 0; } $best = $rows[0]; if ( isset( $w['time'] ) ) { foreach ( $rows as $r ) { if ( abs( $target - $r['t'] ) < abs( $target - $best['t'] ) ) { $best = $r; } } } return $best['id']; } private static function log_rows( array $site, $show, $page = 1, $at = 0, array $w = array(), $fkey = '' ) { if ( empty( $site['id'] ) || empty( $site['verified'] ) ) { return array( 'counts' => array(), 'log' => array(), 'at' => 0, 'page' => 1, 'pages' => 1, 'found' => 0 ); } $page = max( 1, (int) $page ); $target = isset( $w['time'] ) ? self::at_time( $w['time'] ) : 0; $from = $at > 0 ? (int) $at : ( $target ? $target + 600 : 0 ); $base = '/v1/sites/' . rawurlencode( $site['id'] ) . '/log?' . ( '' !== $fkey ? 'fkey=' . rawurlencode( $fkey ) . '&' : '' ) . ( 'all' !== $show ? 'outcome=' . rawurlencode( $show ) . '&' : '' ) . ( isset( $w['ref'] ) ? 'ref=' . $w['ref'] . '&' : '' ) . ( isset( $w['path'] ) ? 'path=' . rawurlencode( $w['path'] ) . '&' : '' ) . ( isset( $w['word'] ) ? 'q=' . rawurlencode( $w['word'] ) . '&' : '' ); $local = self::local_rows( $show, $fkey, $w, $from ); $need = 40 * $page; $rows = array(); $a = null; for ( $skip = 0; $skip < $need; $skip += 100 ) { $n = min( 100, $need - $skip ); $at_q = $a ? $at : $from; $got = Rabbitproof_Api::call( 'GET', $base . 'limit=' . $n . ( $skip > 0 ? '&skip=' . $skip : '' ) . ( $at_q > 0 ? '&before=' . $at_q : '' ), null, 5 ); if ( is_wp_error( $got ) || 200 !== $got['_status'] || empty( $got['ok'] ) ) { return null; } if ( ! $a ) { $a = $got; $at = $at > 0 ? (int) $at : ( isset( $got['before'] ) ? (int) $got['before'] : $from ); } $part = isset( $got['log'] ) && is_array( $got['log'] ) ? $got['log'] : array(); foreach ( $part as $e ) { if ( is_array( $e ) ) { $rows[] = self::row_of( $e ); } } if ( count( $part ) < $n ) { break; } } $total = ( isset( $a['total'] ) ? (int) $a['total'] : 0 ) + count( $local ); $counts = isset( $a['counts'] ) && is_array( $a['counts'] ) ? $a['counts'] : array(); if ( $local ) { foreach ( array( 'all', 'blocked' ) as $c ) { $counts[ $c ] = ( isset( $counts[ $c ] ) ? (int) $counts[ $c ] : 0 ) + count( $local ); } $rows = array_merge( $rows, $local ); usort( $rows, function ( $x, $y ) { return $y['t'] - $x['t']; } ); } $rows = array_slice( self::bursts( $rows ), 40 * ( $page - 1 ), 40 ); $pages = max( 1, (int) ceil( $total / 40 ), $page ); return array( 'counts' => $counts, 'log' => $rows, 'at' => $at, 'page' => $page, 'pages' => $pages, 'found' => self::found_in( $rows, $w, $target ) ); } private static function local_rows( $show, $fkey, array $w, $from ) { if ( ! in_array( $show, array( 'all', 'blocked' ), true ) || '' !== $fkey || isset( $w['ref'] ) || isset( $w['time'] ) ) { return array(); } $find = isset( $w['word'] ) ? strtolower( (string) $w['word'] ) : ( isset( $w['path'] ) ? strtolower( (string) $w['path'] ) : '' ); $out = array(); foreach ( Rabbitproof_Probes::rows() as $r ) { if ( $from > 0 && $r['t'] >= $from ) { continue; } if ( '' !== $find && false === strpos( strtolower( $r['why'] . ' ' . $r['path'] ), $find ) ) { continue; } $out[] = $r; } return $out; } public static function place( array $args ) { $args = array_filter( $args, function ( $v ) { return '' !== (string) $v && '0' !== (string) $v; } ); return add_query_arg( array_merge( array( 'page' => self::SLUG ), $args ), admin_url( 'admin.php' ) ); } private static function back_place() { $raw = isset( $_POST['back'] ) ? sanitize_text_field( wp_unslash( $_POST['back'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in the handler (check_admin_referer). $b = array(); parse_str( $raw, $b ); $str = function ( $k ) use ( $b ) { return isset( $b[ $k ] ) && is_string( $b[ $k ] ) ? $b[ $k ] : ''; }; $args = array( 'show' => isset( self::OUTCOMES[ $str( 'show' ) ] ) && 'all' !== $str( 'show' ) ? $str( 'show' ) : '', 'q' => self::search_arg( self::read_search( $str( 'q' ) ) ), 'fkey' => isset( self::forms()[ $str( 'fkey' ) ] ) ? $str( 'fkey' ) : '', ); return array( 'log', array_filter( $args, 'strlen' ) ); } private static function page_list( $page, $pages ) { $out = array(); for ( $i = 1; $i <= $pages; $i++ ) { if ( 1 === $i || $pages === $i || abs( $i - $page ) <= 2 ) { if ( $out && end( $out ) < $i - 1 ) { $out[] = 0; } $out[] = $i; } } return $out; } public static function save_protect() { self::guard(); check_admin_referer( 'rabbitproof_protect' ); if ( isset( $_POST['part'] ) && 'files' === sanitize_key( wp_unslash( $_POST['part'] ) ) ) { Rabbitproof_Options::update( array( 'scanners' => ! empty( $_POST['scanners'] ), 'users' => ! empty( $_POST['users'] ), 'versions' => ! empty( $_POST['versions'] ) ) ); Rabbitproof_Files::apply( Rabbitproof_Files::wanted() ); self::back( 'protect', 'saved' ); } $raw = isset( $_POST['forms'] ) && is_array( $_POST['forms'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['forms'] ) ) : array(); $posted = array_map( 'sanitize_key', array_keys( $raw ) ); $forms = array(); foreach ( array_keys( Rabbitproof_Options::FORMS ) as $form ) { $forms[ $form ] = in_array( $form, $posted, true ); } Rabbitproof_Options::update( array( 'forms' => $forms ) ); self::back( 'forms', 'saved' ); } public static function modes() { return array( 'off' => array( __( 'Off', 'rabbitproof' ), __( 'Nothing is checked.', 'rabbitproof' ) ), 'watch' => array( __( 'Log only', 'rabbitproof' ), __( 'Count them and let everyone in. Bots using a proxy or VPN are remembered, ready for when you block.', 'rabbitproof' ) ), 'enforce' => array( __( 'Block them', 'rabbitproof' ), __( 'Real visitors walk straight in.', 'rabbitproof' ) ), ); } public static function save_mode() { self::guard(); check_admin_referer( 'rabbitproof_mode' ); $mode = isset( $_POST['mode'] ) ? sanitize_key( wp_unslash( $_POST['mode'] ) ) : ''; $o = Rabbitproof_Options::all(); if ( ! isset( self::modes()[ $mode ] ) || empty( $o['site']['id'] ) ) { self::back( 'protect', 'down' ); } $id = rawurlencode( $o['site']['id'] ); $fresh = Rabbitproof_Api::call( 'GET', '/v1/sites/' . $id, null, 10 ); if ( is_wp_error( $fresh ) || 200 !== $fresh['_status'] || ! isset( $fresh['site']['version'] ) ) { self::back( 'protect', 'down' ); } $options = isset( $fresh['site']['options'] ) && is_array( $fresh['site']['options'] ) ? $fresh['site']['options'] : array(); $options['mode'] = $mode; $a = Rabbitproof_Api::call( 'PATCH', '/v1/sites/' . $id . '/options', array( 'options' => $options, 'version' => (int) $fresh['site']['version'] ), 10 ); if ( is_wp_error( $a ) || 200 !== $a['_status'] || empty( $a['site'] ) ) { self::back( 'protect', 'down' ); } Rabbitproof_Options::update( array( 'site' => self::summary( $a['site'] ) ) ); delete_transient( self::WEEK ); self::back( 'protect', 'saved' ); } public static function unblock() { self::guard(); check_admin_referer( 'rabbitproof_unblock' ); list( $tab, $args ) = self::back_place(); $o = Rabbitproof_Options::all(); $row = isset( $_POST['row'] ) ? absint( wp_unslash( $_POST['row'] ) ) : 0; if ( ! $row || empty( $o['site']['id'] ) ) { self::back( $tab, 'down', $args ); } $a = Rabbitproof_Api::call( 'POST', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/log/' . $row . '/let', array(), 10 ); if ( is_wp_error( $a ) || 200 !== $a['_status'] ) { self::back( $tab, 'down', $args ); } $ref = isset( $_POST['ref'] ) ? sanitize_text_field( wp_unslash( $_POST['ref'] ) ) : ''; self::back( $tab, Rabbitproof_Caught::send_for( $ref ) > 0 ? 'unblocked_sent' : 'unblocked', $args ); } public static function block_again() { self::guard(); check_admin_referer( 'rabbitproof_block' ); list( $tab, $args ) = self::back_place(); $o = Rabbitproof_Options::all(); $row = isset( $_POST['row'] ) ? absint( wp_unslash( $_POST['row'] ) ) : 0; if ( ! $row || empty( $o['site']['id'] ) ) { self::back( $tab, 'down', $args ); } $a = Rabbitproof_Api::call( 'POST', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/log/' . $row . '/unlet', array(), 10 ); self::back( $tab, ! is_wp_error( $a ) && 200 === $a['_status'] ? 'blocked' : 'down', $args ); } public static function save_server() { self::guard(); check_admin_referer( 'rabbitproof_server' ); Rabbitproof_Options::update( array( 'proxy' => ! empty( $_POST['proxy'] ), 'xmlrpc_off' => ! empty( $_POST['xmlrpc_off'] ) ) ); self::back( 'protect', 'saved' ); } private static function week( array $site ) { $cached = get_transient( self::WEEK ); if ( is_array( $cached ) && isset( $cached['spam_sends'] ) ) { return $cached; } if ( empty( $site['id'] ) || empty( $site['verified'] ) ) { return null; } $id = rawurlencode( $site['id'] ); $o = Rabbitproof_Api::call( 'GET', '/v1/sites/' . $id . '/overview', null, 5 ); if ( is_wp_error( $o ) || 200 !== $o['_status'] || ! isset( $o['report'] ) ) { return null; } $part = function ( $k, $v ) { return array( '_status' => 200, $k => $v ); }; $report = $part( 'report', $o['report'] ); $usage = $part( 'usage', isset( $o['usage'] ) ? $o['usage'] : null ); if ( isset( $o['site'] ) && is_array( $o['site'] ) ) { Rabbitproof_Options::update( array( 'site' => self::summary( $o['site'] ) ) ); } $week = array( 'fake' => isset( $report['report']['fake'] ) ? (int) $report['report']['fake'] : 0, 'real' => isset( $report['report']['real'] ) ? (int) $report['report']['real'] : 0, 'checked' => isset( $report['report']['checked'] ) ? (int) $report['report']['checked'] : 0, ); $why = function ( $r ) { $out = array(); $sends = ! is_wp_error( $r ) && 200 === $r['_status'] && isset( $r['report']['sends'] ) && is_array( $r['report']['sends'] ) ? $r['report']['sends'] : array(); foreach ( ( ! is_wp_error( $r ) && 200 === $r['_status'] && isset( $r['report']['why'] ) && is_array( $r['report']['why'] ) ? $r['report']['why'] : array() ) as $w ) { if ( is_array( $w ) && isset( $w['label'], $w['n'] ) ) { $out[ (string) $w['label'] ] = ( isset( $out[ (string) $w['label'] ] ) ? $out[ (string) $w['label'] ] : 0 ) + (int) $w['n']; } } foreach ( array( 'spam' => 'Form spam', 'comment' => 'Comment spam' ) as $key => $label ) { if ( isset( $out[ $label ] ) || ! empty( $sends[ $key ] ) ) { $out[ $label ] = isset( $sends[ $key ] ) ? max( 0, (int) $sends[ $key ] ) : 0; } } return $out; }; $week['spammers'] = 0; foreach ( ( ! is_wp_error( $report ) && isset( $report['report']['why'] ) && is_array( $report['report']['why'] ) ? $report['report']['why'] : array() ) as $w ) { if ( is_array( $w ) && isset( $w['label'], $w['n'] ) && in_array( (string) $w['label'], self::SPAM, true ) ) { $week['spammers'] += (int) $w['n']; } } $week['why'] = array( 'day' => $why( $part( 'report', isset( $o['today'] ) ? $o['today'] : null ) ), 'week' => $why( $part( 'report', isset( $o['week'] ) ? $o['week'] : null ) ), 'period' => $why( $report ), ); $local_from = isset( $o['usage']['from'] ) && is_numeric( $o['usage']['from'] ) ? (int) floor( $o['usage']['from'] / 1000 ) : time() - 30 * DAY_IN_SECONDS; foreach ( array( 'day' => strtotime( 'today' ), 'week' => time() - 7 * DAY_IN_SECONDS, 'period' => $local_from ) as $span => $since ) { foreach ( Rabbitproof_Probes::counts( $since ) as $label => $n ) { $week['why'][ $span ][ $label ] = ( isset( $week['why'][ $span ][ $label ] ) ? $week['why'][ $span ][ $label ] : 0 ) + $n; if ( 'period' === $span ) { $week['fake'] += $n; $week['checked'] += $n; } } } $week['bots'] = $week['fake'] - min( $week['spammers'], $week['fake'] ); $week['spam'] = 0; foreach ( self::SPAM as $label ) { $week['spam'] += isset( $week['why']['period'][ $label ] ) ? $week['why']['period'][ $label ] : 0; } unset( $week['spammers'] ); $week['spam_sends'] = true; $week['latest'] = array(); $lg = $part( 'log', isset( $o['log'] ) ? $o['log'] : null ); foreach ( ( ! is_wp_error( $lg ) && 200 === $lg['_status'] && isset( $lg['log'] ) && is_array( $lg['log'] ) ? array_slice( $lg['log'], 0, 5 ) : array() ) as $e ) { if ( is_array( $e ) ) { $week['latest'][] = array( 't' => isset( $e['t'] ) ? (int) $e['t'] : 0, 'n' => isset( $e['n'] ) ? max( 1, (int) $e['n'] ) : 1, 'kind' => isset( $e['kind'] ) ? (string) $e['kind'] : '', 'why' => isset( $e['why'] ) ? (string) $e['why'] : '', 'form' => isset( $e['form'] ) && 1 === preg_match( '/^[a-z0-9]{1,20}$/', (string) $e['form'] ) ? (string) $e['form'] : '', 'path' => isset( $e['path'] ) && '' !== $e['path'] ? (string) $e['path'] : '/', 'cc' => isset( $e['cc'] ) ? (string) $e['cc'] : '', 'ref' => isset( $e['ref'] ) && 1 === preg_match( '/^[0-9a-f]{6}$/', (string) $e['ref'] ) ? (string) $e['ref'] : '', 'outcome' => isset( $e['outcome'] ) ? (string) $e['outcome'] : '', 'blocked' => ! empty( $e['blocked'] ), 'addrs' => isset( $e['addrs'] ) ? max( 0, (int) $e['addrs'] ) : 0, 'first' => isset( $e['first'] ) ? (int) $e['first'] : 0, 'id' => isset( $e['id'] ) ? (int) $e['id'] : 0, ); } } $u = ! is_wp_error( $usage ) && isset( $usage['usage'] ) && is_array( $usage['usage'] ) ? $usage['usage'] : array(); $week['used'] = isset( $u['used'] ) ? (int) $u['used'] : 0; $week['included'] = isset( $u['included'] ) ? (int) $u['included'] : 0; $week['over'] = ! empty( $u['over'] ); $week['extra_cents'] = isset( $u['extra_cents'] ) ? (int) $u['extra_cents'] : 0; $week['extra'] = isset( $u['extra'] ) ? max( 0, (int) $u['extra'] ) : max( 0, $week['used'] - $week['included'] ); $week['billing'] = ! empty( $u['billing'] ); $week['days'] = array(); foreach ( array_slice( isset( $u['days'] ) && is_array( $u['days'] ) ? $u['days'] : array(), 0, 40 ) as $d ) { if ( is_array( $d ) && isset( $d[0], $d[1] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', (string) $d[0] ) ) { $week['days'][] = array( (string) $d[0], (int) $d[1] ); } } $week['resets'] = isset( $u['resets'] ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $u['resets'] ) ? $u['resets'] : ''; $week['from'] = isset( $u['from'] ) && is_numeric( $u['from'] ) ? (int) floor( $u['from'] / 1000 ) : 0; $week['to'] = isset( $u['to'] ) && is_numeric( $u['to'] ) ? (int) floor( $u['to'] / 1000 ) : 0; $week['start'] = isset( $u['start'] ) && is_array( $u['start'] ) ? array( 'cap' => (int) $u['start']['cap'], 'used' => (int) $u['start']['used'], 'ended' => ! empty( $u['start']['ended'] ), 'card' => ! empty( $u['start']['card'] ) ) : null; foreach ( array_slice( Rabbitproof_Probes::rows(), 0, 5 ) as $e ) { $week['latest'][] = $e; } usort( $week['latest'], function ( $x, $y ) { return $y['t'] - $x['t']; } ); $week['latest'] = array_slice( self::bursts( $week['latest'] ), 0, 5 ); set_transient( self::WEEK, $week, 10 * MINUTE_IN_SECONDS ); return $week; } private static function status( array $o, $week = null ) { $site = $o['site']; $down = (int) get_option( 'rabbitproof_unreachable', 0 ); if ( ! Rabbitproof_Options::connected() || empty( $site['id'] ) ) { return array( 'warn', __( 'Not connected', 'rabbitproof' ), __( 'Connect this website to start.', 'rabbitproof' ), __( 'Connect', 'rabbitproof' ), 'keys', 'keys' ); } if ( $down && time() - $down < HOUR_IN_SECONDS ) { return array( 'err', __( "Can't reach Rabbitproof", 'rabbitproof' ), __( "Forms went through unchecked while we couldn't be reached. Your website is fine.", 'rabbitproof' ), '', '', 'down' ); } if ( isset( $site['tier'] ) && 'none' === $site['tier'] ) { $says = isset( $site['first_left'] ) && false === $site['first_left'] ? __( 'This website has had its first 5,000 requests before. Every website gets these once. To keep using Rabbitproof, sign in to your dashboard and add a payment method.', 'rabbitproof' ) : __( "This website isn't set up yet. Sign in to your dashboard to continue.", 'rabbitproof' ); return array( 'warn', __( 'Not started', 'rabbitproof' ), $says, __( 'Go to your dashboard', 'rabbitproof' ), 'dash', 'start' ); } if ( empty( $site['verified'] ) ) { return array( 'warn', __( 'Not verified', 'rabbitproof' ), __( 'Prove this website is yours to start.', 'rabbitproof' ), __( 'Try again', 'rabbitproof' ), 'verify', 'verify' ); } if ( is_array( $week ) && ! empty( $week['start'] ) && $week['start']['ended'] ) { return array( 'err', __( 'Paused', 'rabbitproof' ), __( 'Your first requests are over. Forms go through without a check until you set up a payment method in your dashboard.', 'rabbitproof' ), __( 'Set up payment method', 'rabbitproof' ), 'dash', 'paused' ); } if ( is_array( $week ) && $week['over'] ) { $until = $week['resets'] ? date_i18n( 'j M', strtotime( $week['resets'] . ' 00:00:00 UTC' ) ) : __( 'the billing period renews', 'rabbitproof' ); /* translators: %s: the day the billing period renews. */ return array( 'err', __( 'Out of requests', 'rabbitproof' ), sprintf( __( "This billing period's requests are used up. Forms go through unchecked until %s.", 'rabbitproof' ), $until ), __( 'See billing', 'rabbitproof' ), 'dash', 'over' ); } if ( 'off' === $site['mode'] ) { return array( 'off', '', '', '', 'protect', 'off' ); } if ( 'enforce' !== $site['mode'] ) { return array( 'warn', '', '', '', 'protect', 'log' ); } return array( '', '', '', '', 'protect', 'on' ); } private static function link( array $status ) { switch ( isset( $status[5] ) ? $status[5] : '' ) { case 'keys': return array( 'warn', __( 'Not connected', 'rabbitproof' ), 'keys' ); case 'down': return array( 'err', __( 'Not connected', 'rabbitproof' ), '' ); case 'start': case 'verify': return array( 'warn', __( 'Not protected', 'rabbitproof' ), $status[4] ); case 'paused': case 'over': return array( 'err', __( 'Not protected', 'rabbitproof' ), $status[4] ); case 'off': return array( 'off', __( 'Not protected', 'rabbitproof' ), 'protect' ); case 'log': return array( 'warn', __( 'Log only', 'rabbitproof' ), 'protect' ); default: return array( '', __( 'Protected', 'rabbitproof' ), 'protect' ); } } private static function note( $key ) { $notes = array( 'connected_button' => array( 'ok', __( 'Your Rabbitproof account is connected.', 'rabbitproof' ) ), 'connect_again' => array( 'err', __( "That didn't finish. Press Connect to Rabbitproof again.", 'rabbitproof' ) ), 'connect_cancelled' => array( 'warn', __( "Not connected yet. Press Connect to Rabbitproof when you're ready.", 'rabbitproof' ) ), 'cleanup_removed' => array( 'ok', __( 'Removed.', 'rabbitproof' ) ), 'cleanup_kept' => array( 'err', __( 'It could not be removed from here. Please delete it from your website.', 'rabbitproof' ) ), 'verified' => array( 'ok', __( 'Connected and verified. Rabbitproof is now checking this website.', 'rabbitproof' ) ), 'saved' => array( 'ok', __( 'Saved.', 'rabbitproof' ) ), 'lock_locked' => array( 'ok', __( 'Locked. Your server only answers visitors that came through Rabbitproof.', 'rabbitproof' ) ), 'unlocked' => array( 'ok', __( 'Unlocked. Your server answers everyone again.', 'rabbitproof' ) ), 'lock_notlive' => array( 'err', __( 'Not yet: your website is not answering through Rabbitproof. Finish Connect your site in your dashboard first.', 'rabbitproof' ) ), 'lock_leaving' => array( 'err', __( 'Not while a move away from Bot Management is waiting.', 'rabbitproof' ) ), 'lock_nofile' => array( 'err', __( 'WordPress could not change your .htaccess file. The lines to add yourself are in your dashboard, under Connect your site.', 'rabbitproof' ) ), 'lock_broke' => array( 'err', __( 'Your website stopped answering with the lock on, so it was taken off again at once. Nothing else changed.', 'rabbitproof' ) ), 'lock_nolock' => array( 'err', __( 'Your server did not use the lines, so they were taken out again. Ask your host whether .htaccess rewrite rules are turned on.', 'rabbitproof' ) ), 'lock_down' => array( 'err', __( 'Rabbitproof could not be reached. Try again in a minute.', 'rabbitproof' ) ), 'badpath' => array( 'err', __( 'An address starts with / and has no spaces, such as /webhooks/stripe.', 'rabbitproof' ) ), 'badplace' => array( 'err', __( 'Choose a region or a country from the list.', 'rabbitproof' ) ), 'badip' => array( 'err', __( 'Type an IP address, like 203.0.113.24.', 'rabbitproof' ) ), 'kept' => array( 'ok', __( 'Kept. That visitor is always allowed until you remove them.', 'rabbitproof' ) ), 'removed' => array( 'ok', __( 'Removed. That visitor is checked like everyone else from now.', 'rabbitproof' ) ), 'geo_open' => array( 'ok', __( 'Add a region or a country to start blocking.', 'rabbitproof' ) ), 'blocked' => array( 'ok', __( 'Blocked again. That visitor is checked like everyone else from now.', 'rabbitproof' ) ), 'unblocked' => array( 'ok', __( 'Unblocked. For the next 7 days this visitor can visit your whole website and send any form, with no check.', 'rabbitproof' ) ), 'unblocked_sent' => array( 'ok', __( 'Unblocked. For the next 7 days this visitor can visit your whole website and send any form, with no check. Their message has been emailed to you.', 'rabbitproof' ) ), 'wrongkey' => array( 'err', __( 'Rabbitproof did not accept this website. Press Connect to Rabbitproof to try again.', 'rabbitproof' ) ), 'mismatch' => array( 'err', __( 'Something went wrong connecting this website. Press Connect to Rabbitproof to try again.', 'rabbitproof' ) ), 'down' => array( 'err', __( 'Rabbitproof could not be reached. Try again in a minute.', 'rabbitproof' ) ), 'gone' => array( 'warn', __( 'This website is no longer in your Rabbitproof account, so nothing is checked. Your forms work as normal. To protect it again, add it in your Rabbitproof dashboard, then press Connect to Rabbitproof here.', 'rabbitproof' ) ), 'noproof' => array( 'warn', __( 'Connected, but we could not read the proof on your website, so it is not verified yet. Your host may block the .well-known folder. Press Connect to try again, or ask us in Support in your dashboard.', 'rabbitproof' ) ), 'oldproof' => array( 'warn', __( 'Connected, but an old file is in the way: delete rabbitproof-verify.txt from the .well-known folder in the main folder of your website, then press Connect again.', 'rabbitproof' ) ), ); return isset( $notes[ $key ] ) ? $notes[ $key ] : null; } private static function icon( $name ) { echo ''; } public static function page() { if ( ! current_user_can( self::CAP ) ) { return; } $o = Rabbitproof_Options::all(); $tabs = array( 'overview' => array( __( 'Overview', 'rabbitproof' ), 'home' ), 'log' => array( __( 'Requests', 'rabbitproof' ), 'list' ), 'forms' => array( __( 'Forms', 'rabbitproof' ), 'forms' ), 'protect' => array( __( 'Protection', 'rabbitproof' ), 'shield' ), 'allow' => array( __( 'Allow list', 'rabbitproof' ), 'allow' ), 'billing' => array( __( 'Billing', 'rabbitproof' ), 'card' ), ); $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'overview'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- choosing a tab changes nothing. $keys_open = 'keys' === $tab; $tab = 'checks' === $tab ? 'protect' : $tab; $tab = isset( $tabs[ $tab ] ) ? $tab : 'overview'; $note = isset( $_GET['rp'] ) ? self::note( sanitize_key( wp_unslash( $_GET['rp'] ) ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['fresh'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended delete_transient( self::WEEK ); } $o = self::state( $o ); if ( Rabbitproof_Options::connected() && ! empty( $o['site']['id'] ) && empty( $o['site']['verified'] ) && ! get_transient( 'rabbitproof_verify_asked' ) ) { set_transient( 'rabbitproof_verify_asked', 1, MINUTE_IN_SECONDS ); if ( self::verify_now( $o['site']['id'] ) ) { $o = Rabbitproof_Options::all(); } } $idle = Rabbitproof_Options::connected() && ! empty( $o['site']['id'] ) && isset( $o['site']['tier'] ) && 'none' === $o['site']['tier']; if ( ! isset( $tabs[ $tab ] ) || $idle ) { $tab = 'overview'; } if ( Rabbitproof_Options::gone() ) { $note = $note ? $note : self::note( 'gone' ); } $week = in_array( $tab, array( 'overview', 'billing' ), true ) ? self::week( $o['site'] ) : get_transient( self::WEEK ); $week = is_array( $week ) ? $week : null; if ( $note && isset( $_GET['rp'] ) && 'connected_button' === $_GET['rp'] && $week && ! empty( $week['start'] ) && empty( $week['start']['card'] ) && empty( $week['start']['ended'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a message key. /* translators: %s: how many first requests. */ $note[1] .= ' ' . sprintf( __( "You're using your first %s requests: the Billing tab shows how many are left.", 'rabbitproof' ), number_format_i18n( (int) $week['start']['cap'] ) ); } if ( in_array( $tab, array( 'protect', 'allow' ), true ) && ! empty( $o['site']['id'] ) ) { $fresh = Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $o['site']['id'] ), null, 5 ); if ( ! is_wp_error( $fresh ) && 200 === $fresh['_status'] && isset( $fresh['site'] ) && is_array( $fresh['site'] ) ) { Rabbitproof_Options::update( array( 'site' => self::summary( $fresh['site'] ) ) ); $o = Rabbitproof_Options::all(); } } $show = isset( $_GET['show'] ) ? sanitize_key( wp_unslash( $_GET['show'] ) ) : 'all'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a filter changes nothing. $show = isset( self::OUTCOMES[ $show ] ) ? $show : 'all'; $rp_pg = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- paging changes nothing. $rp_at = isset( $_GET['at'] ) ? absint( $_GET['at'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $rp_q = isset( $_GET['q'] ) ? sanitize_text_field( wp_unslash( $_GET['q'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a search changes nothing. $rp_w = self::read_search( $rp_q ); $rp_fk = isset( $_GET['fkey'] ) ? sanitize_key( wp_unslash( $_GET['fkey'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- a filter changes nothing. $rp_fk = isset( self::forms()[ $rp_fk ] ) ? $rp_fk : ''; $log = 'log' === $tab && empty( $rp_w['bad'] ) ? self::log_rows( $o['site'], $show, $rp_pg, $rp_at, $rp_w, $rp_fk ) : null; $visitors = null; if ( 'allow' === $tab && ! empty( $o['site']['id'] ) ) { $v = Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/allowlist', null, 5 ); $visitors = ! is_wp_error( $v ) && 200 === $v['_status'] && isset( $v['visitors'] ) && is_array( $v['visitors'] ) ? $v['visitors'] : null; } $always = 'allow' === $tab && isset( $o['site']['bm'] ) && is_array( $o['site']['bm'] ) ? self::always_rows( $o['site']['bm'], $o['site'] ) : array( 'allowed' => array(), 'waiting' => array() ); $kept = 'log' === $tab && is_array( $log ) && ! empty( $log['log'] ) ? Rabbitproof_Caught::recent() : array(); $comments = 'log' === $tab && is_array( $log ) && ! empty( $log['log'] ) ? self::comments_of( $log['log'] ) : ( 'overview' === $tab && is_array( $week ) && ! empty( $week['latest'] ) ? self::comments_of( $week['latest'] ) : array() ); $base = admin_url( 'admin.php?page=' . self::SLUG ); $off = ! Rabbitproof_Options::connected() || empty( $o['site']['id'] ); $gone = $off && Rabbitproof_Options::gone(); $signup = 'https://rabbitproof.com.au/sign-up?website=' . rawurlencode( (string) wp_parse_url( home_url( '/' ), PHP_URL_HOST ) ); $status = self::status( $o, $week ); $link = self::link( $status ); $old = ! $off && 'overview' === $tab ? self::leftover( $o ) : null; require RABBITPROOF_DIR . 'includes/view-admin.php'; } private static function leftover( array $o ) { if ( ! isset( $o['site']['tier'] ) || 'advanced' !== $o['site']['tier'] || empty( $o['site']['id'] ) ) { return null; } if ( array() !== Rabbitproof_Cleanup::found() ) { return array( 'fix' => true, 'line' => Rabbitproof_Cleanup::line() ); } $seen = get_transient( 'rabbitproof_cleanup_line' ); if ( false === $seen ) { $c = Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $o['site']['id'] ) . '/connection', null, 10 ); $ol = ! is_wp_error( $c ) && 200 === $c['_status'] && isset( $c['oldline'] ) && is_array( $c['oldline'] ) ? $c['oldline'] : array(); $seen = ! empty( $ol['seen'] ) && (int) $ol['seen'] > ( isset( $ol['clear'] ) ? (int) $ol['clear'] : 0 ) ? 'yes' : 'no'; set_transient( 'rabbitproof_cleanup_line', $seen, 10 * MINUTE_IN_SECONDS ); } return 'yes' === $seen ? array( 'fix' => false, 'line' => Rabbitproof_Cleanup::line() ) : null; } private static function state( array $o ) { if ( ! empty( $o['site']['id'] ) && Rabbitproof_Options::valid_secret_key( $o['secret_key'] ) ) { $fresh = Rabbitproof_Api::call( 'GET', '/v1/sites/' . rawurlencode( $o['site']['id'] ), null, 5 ); if ( ! is_wp_error( $fresh ) && 200 === $fresh['_status'] && isset( $fresh['site'] ) && is_array( $fresh['site'] ) ) { $now = self::summary( $fresh['site'] ); foreach ( array( 'tier', 'extras', 'verified', 'first_left' ) as $k ) { if ( ( isset( $o['site'][ $k ] ) ? $o['site'][ $k ] : null ) !== $now[ $k ] ) { delete_transient( self::WEEK ); break; } } Rabbitproof_Options::update( array( 'site' => $now ) ); $o = Rabbitproof_Options::all(); } } return $o; } private static function target( $to ) { if ( 'verify' === $to ) { return wp_nonce_url( admin_url( 'admin-post.php?action=rabbitproof_verify' ), 'rabbitproof_verify' ); } return in_array( $to, array( 'keys', 'protect' ), true ) ? admin_url( 'admin.php?page=' . self::SLUG . '&tab=' . $to ) : self::dashboard(); } public static function dashboard() { $o = Rabbitproof_Options::all(); $id = isset( $o['site']['id'] ) ? (string) $o['site']['id'] : ''; return 1 === preg_match( '/^[0-9a-f]{16}$/', $id ) ? self::DASHBOARD . '?site=' . $id : self::DASHBOARD; } }