ip_list( $list, $assoc ); }, array( 'shortdesc' => 'allowlist' === $list ? 'List the addresses that skip every check.' : 'List the addresses that are always refused.' ) ); \WP_CLI::add_command( "spam-eater $list add", function ( $args ) use ( $cli, $list ) { $cli->ip_list_change( $list, $args, true ); }, array( 'shortdesc' => 'allowlist' === $list ? 'Add addresses or CIDR ranges that skip every check.' : 'Add addresses or CIDR ranges that are always refused (never at login).', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'address', 'optional' => false, 'repeating' => true, ), ), ) ); \WP_CLI::add_command( "spam-eater $list remove", function ( $args ) use ( $cli, $list ) { $cli->ip_list_change( $list, $args, false ); }, array( 'shortdesc' => 'Remove addresses or CIDR ranges from the list.', 'synopsis' => array( array( 'type' => 'positional', 'name' => 'address', 'optional' => false, 'repeating' => true, ), ), ) ); } \WP_CLI::add_command( 'spam-eater login-waits clear', array( $cli, 'login_waits_clear' ) ); \WP_CLI::add_command( 'spam-eater check', array( $cli, 'check' ) ); } /** * The plugin. * * @return Plugin */ private function plugin() { return Plugin::instance(); } /** * Show how many submissions were refused, and why. * * ## OPTIONS * * [--days=] * : Days to cover, today included (1 to 35). * --- * default: 30 * --- * * [--by=] * : Group by reason or by day. * --- * default: reason * options: * - reason * - day * --- * * [--format=] * : Output format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * ## EXAMPLES * * wp spam-eater stats --days=7 * wp spam-eater stats --by=day --format=csv * * @param array $args Positional arguments. * @param array $assoc Options. */ public function stats( $args, $assoc ) { $days = max( 1, min( Plugin::STATS_DAYS, (int) \WP_CLI\Utils\get_flag_value( $assoc, 'days', 30 ) ) ); $totals = Dashboard::totals( $this->plugin(), $days ); $format = \WP_CLI\Utils\get_flag_value( $assoc, 'format', 'table' ); $rows = array(); if ( 'day' === \WP_CLI\Utils\get_flag_value( $assoc, 'by', 'reason' ) ) { foreach ( $totals['days'] as $day => $count ) { $rows[] = array( 'day' => $day, 'refused' => $count, ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'day', 'refused' ) ); } else { foreach ( $totals['reasons'] as $reason => $count ) { $rows[] = array( 'reason' => $reason, 'label' => $this->plugin()->reason_label( $reason ), 'refused' => $count, ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'reason', 'label', 'refused' ) ); } if ( 'table' === $format ) { \WP_CLI::log( sprintf( 'Refused in the last %d days: %d. Login waits started: %d. Refused since installed: %d.', $days, $totals['total'], $totals['throttled'], $this->plugin()->get_blocked_total() ) ); } } /** * Take an address off the automatic block list. * * The list only holds a keyed hash of each address, so the address you give is * hashed the same way and looked up. * * ## OPTIONS * * * : IP address (IPv4 or IPv6). * * ## EXAMPLES * * wp spam-eater unblock 203.0.113.7 * * @param array $args Positional arguments. */ public function unblock( $args ) { $ip = $this->plugin()->normalise_ip_public( $args[0] ); if ( '' === $ip ) { \WP_CLI::error( sprintf( '%s is not an IP address.', $args[0] ) ); } if ( $this->plugin()->unblock_ip( $ip ) ) { \WP_CLI::success( sprintf( 'Unblocked %s.', $ip ) ); } else { \WP_CLI::warning( sprintf( '%s is not on the block list.', $ip ) ); } if ( 'SE-H0' === $this->plugin()->dry_run( array(), $ip, 'basic' ) ) { \WP_CLI::warning( sprintf( '%s is also covered by your own block list. See: wp spam-eater blocklist list', $ip ) ); } } /** * List the automatic block list: anonymised addresses only. * * ## OPTIONS * * [--all] * : Also list addresses that are counting towards a block but not blocked yet. * * [--format=] * : Output format. * --- * default: table * options: * - table * - csv * - json * - yaml * - count * --- * * @param array $args Positional arguments. * @param array $assoc Options. */ public function blocked_list( $args, $assoc ) { $all = (bool) \WP_CLI\Utils\get_flag_value( $assoc, 'all', false ); $rows = array(); foreach ( $this->plugin()->get_block_list() as $key => $entry ) { if ( ! $all && empty( $entry['blocked_at'] ) ) { continue; } $rows[] = array( 'address' => isset( $entry['label'] ) ? $entry['label'] : '', 'attempts' => isset( $entry['count'] ) ? (int) $entry['count'] : 0, 'blocked_since' => ! empty( $entry['blocked_at'] ) ? wp_date( 'Y-m-d H:i', (int) $entry['blocked_at'] ) : '', 'key' => $key, ); } \WP_CLI\Utils\format_items( \WP_CLI\Utils\get_flag_value( $assoc, 'format', 'table' ), $rows, array( 'address', 'attempts', 'blocked_since', 'key' ) ); } /** * Empty the automatic block list. * * ## OPTIONS * * [--yes] * : Don't ask. * * @param array $args Positional arguments. * @param array $assoc Options. */ public function blocked_clear( $args, $assoc ) { \WP_CLI::confirm( 'Remove every address from the automatic block list?', $assoc ); $this->plugin()->clear_block_list(); \WP_CLI::success( 'The block list is empty.' ); } /** * Show one setting, or all of them. * * ## OPTIONS * * [] * : Setting name, for example ip_block_threshold, block_words or wpforms. * * [--format=] * : Output format. * --- * default: table * options: * - table * - json * - yaml * --- * * @param array $args Positional arguments. * @param array $assoc Options. */ public function settings_get( $args, $assoc ) { $settings = $this->plugin()->get_all_settings(); if ( empty( $args ) ) { $this->settings_list( $args, $assoc ); return; } if ( ! array_key_exists( $args[0], $settings ) ) { \WP_CLI::error( sprintf( 'There is no setting called %s. See: wp spam-eater settings list', $args[0] ) ); } $value = $settings[ $args[0] ]; if ( 'json' === \WP_CLI\Utils\get_flag_value( $assoc, 'format', 'table' ) ) { \WP_CLI::log( (string) wp_json_encode( $value ) ); return; } \WP_CLI::log( is_array( $value ) ? implode( "\n", $value ) : (string) $value ); } /** * List every setting and its value. * * ## OPTIONS * * [--format=] * : Output format. * --- * default: table * options: * - table * - csv * - json * - yaml * --- * * @param array $args Positional arguments. * @param array $assoc Options. */ public function settings_list( $args, $assoc ) { $format = \WP_CLI\Utils\get_flag_value( $assoc, 'format', 'table' ); if ( 'json' === $format ) { \WP_CLI::log( (string) wp_json_encode( $this->plugin()->get_all_settings() ) ); return; } $rows = array(); foreach ( $this->plugin()->get_all_settings() as $key => $value ) { $rows[] = array( 'setting' => $key, 'value' => is_array( $value ) ? implode( ', ', $value ) : str_replace( "\n", ' | ', (string) $value ), ); } \WP_CLI\Utils\format_items( $format, $rows, array( 'setting', 'value' ) ); } /** * Change a setting. The value is cleaned exactly as on the settings page. * * ## OPTIONS * * * : Setting name (see `wp spam-eater settings list`). * * * : New value. On/off settings take 1 or 0. For lists (trusted_proxies, ip_allowlist, * ip_blocklist, block_words) separate entries with new lines or commas, or pass - to * read the value from standard input. * * ## EXAMPLES * * wp spam-eater settings set login_throttle 1 * wp spam-eater settings set trusted_proxies "173.245.48.0/20,103.21.244.0/22" * cat words.txt | wp spam-eater settings set block_words - * * @param array $args Positional arguments. */ public function settings_set( $args ) { list( $key, $value ) = $args; if ( '-' === $value ) { $value = (string) file_get_contents( 'php://stdin' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading standard input. } if ( in_array( $key, array( 'trusted_proxies', 'ip_allowlist', 'ip_blocklist' ), true ) ) { $value = str_replace( ',', "\n", $value ); } $saved = $this->plugin()->update_settings( array( $key => $value ) ); if ( is_wp_error( $saved ) ) { \WP_CLI::error( $saved->get_error_message() ); } foreach ( get_settings_errors( 'accessnow_spam_eater_settings' ) as $error ) { \WP_CLI::warning( wp_strip_all_tags( $error['message'] ) ); } $now = $saved[ $key ]; \WP_CLI::success( sprintf( '%s is now: %s', $key, is_array( $now ) ? implode( ', ', $now ) : str_replace( "\n", ' | ', (string) $now ) ) ); } /** * Print an IP list. * * @param string $list 'allowlist' or 'blocklist'. * @param array $assoc Options. */ public function ip_list( $list, $assoc ) { $key = 'allowlist' === $list ? 'ip_allowlist' : 'ip_blocklist'; $rows = array(); foreach ( (array) $this->plugin()->get_all_settings()[ $key ] as $entry ) { $rows[] = array( 'address' => $entry ); } \WP_CLI\Utils\format_items( \WP_CLI\Utils\get_flag_value( $assoc, 'format', 'table' ), $rows, array( 'address' ) ); } /** * Add to or remove from an IP list. * * @param string $list 'allowlist' or 'blocklist'. * @param string[] $entries Addresses or ranges. * @param bool $add Add (true) or remove (false). */ public function ip_list_change( $list, $entries, $add ) { $key = 'allowlist' === $list ? 'ip_allowlist' : 'ip_blocklist'; $current = (array) $this->plugin()->get_all_settings()[ $key ]; foreach ( $entries as $entry ) { $clean = $this->plugin()->clean_ip_entry( $entry ); if ( '' === $clean ) { \WP_CLI::error( sprintf( '%s is not an IP address or CIDR range.', $entry ) ); } if ( $add ) { $current[] = $clean; } else { $before = count( $current ); $current = array_values( array_diff( $current, array( $clean ) ) ); if ( count( $current ) === $before ) { \WP_CLI::warning( sprintf( '%s is not on the %s.', $clean, $list ) ); } } } $saved = $this->plugin()->update_settings( array( $key => array_values( array_unique( $current ) ) ) ); if ( is_wp_error( $saved ) ) { \WP_CLI::error( $saved->get_error_message() ); } \WP_CLI::success( sprintf( 'The %s now holds %d entries.', $list, count( $saved[ $key ] ) ) ); } /** * Clear every login wait started by the login throttle. */ public function login_waits_clear() { $this->plugin()->clear_all_login_throttles(); \WP_CLI::success( 'No login has to wait now.' ); } /** * Check what Spam Eater would do with a submission, without recording anything. * * By default it checks a submission like a bot's (no hidden-field values). With * --genuine it adds valid values, so only the address lists and your own rules can * refuse it: useful for testing words, patterns and throwaway domains. * * ## OPTIONS * * [--ip=] * : Address to check as. * --- * default: 192.0.2.1 * --- * * [--text=] * : Text in the submission (in memory only). * * [--email=] * : Email address in the submission. * * [--genuine] * : Include valid hidden-field values, as the script would. * * [--context=] * : Which checks: full, basic or login. * --- * default: full * --- * * ## EXAMPLES * * wp spam-eater check --genuine --text="Cheap casino bonus" * wp spam-eater check --genuine --email=someone@mailinator.com * wp spam-eater check --ip=203.0.113.7 * * @param array $args Positional arguments. * @param array $assoc Options. */ public function check( $args, $assoc ) { $data = array(); if ( \WP_CLI\Utils\get_flag_value( $assoc, 'genuine', false ) ) { $data = $this->plugin()->genuine_fields(); } if ( isset( $assoc['text'] ) ) { $data['message'] = (string) $assoc['text']; } if ( isset( $assoc['email'] ) ) { $data['email'] = (string) $assoc['email']; } $code = $this->plugin()->dry_run( $data, (string) \WP_CLI\Utils\get_flag_value( $assoc, 'ip', '192.0.2.1' ), (string) \WP_CLI\Utils\get_flag_value( $assoc, 'context', 'full' ) ); if ( '' === $code ) { \WP_CLI::success( 'It would be let through.' ); } else { \WP_CLI::log( sprintf( 'It would be refused: %s.', $code ) ); } } }