prefix . 'ajsec_log'; } /** * Create or update the table. */ public static function install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table(); $charset = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, created_at datetime NOT NULL, event varchar(40) NOT NULL DEFAULT '', user_id bigint(20) unsigned NOT NULL DEFAULT 0, username varchar(100) NOT NULL DEFAULT '', ip varchar(45) NOT NULL DEFAULT '', detail varchar(255) NOT NULL DEFAULT '', PRIMARY KEY (id), KEY event (event), KEY created_at (created_at) ) {$charset};" ); update_option( 'ajsec_db_version', self::DB_VERSION, false ); } /** * Event labels. * * @return array */ public static function event_labels() { return array( 'login_success' => __( 'Login', '007aj-security-scanner-firewall' ), 'login_failed' => __( 'Failed login', '007aj-security-scanner-firewall' ), 'lockout' => __( 'Lockout', '007aj-security-scanner-firewall' ), 'lockout_cleared' => __( 'Lockout removed', '007aj-security-scanner-firewall' ), 'waf_block' => __( 'Firewall block', '007aj-security-scanner-firewall' ), 'xmlrpc_blocked' => __( 'XML-RPC blocked', '007aj-security-scanner-firewall' ), 'rate_limited' => __( 'Login rate limit', '007aj-security-scanner-firewall' ), 'user_enum' => __( 'User enumeration blocked', '007aj-security-scanner-firewall' ), 'scan_completed' => __( 'Integrity scan', '007aj-security-scanner-firewall' ), 'settings_changed' => __( 'Settings changed', '007aj-security-scanner-firewall' ), ); } /** * Whether logging is on. * * @return bool */ public static function enabled() { return (bool) Settings::get( 'activity_log' ); } /** * Add a row (only when logging is on). * * @param string $event Event key. * @param string $detail Short detail text. * @param int $user_id User ID, 0 if none. * @param string $username Username involved, if any. * @param string $ip IP address; defaults to the current request. * @return bool */ public static function add( $event, $detail = '', $user_id = 0, $username = '', $ip = null ) { global $wpdb; if ( ! self::enabled() ) { return false; } $ok = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table. self::table(), array( 'created_at' => current_time( 'mysql', true ), 'event' => substr( sanitize_key( $event ), 0, 40 ), 'user_id' => absint( $user_id ), 'username' => self::text( sanitize_user( self::text( $username, 400 ), false ), 100 ), 'ip' => substr( null === $ip ? Request::ip() : (string) $ip, 0, 45 ), 'detail' => self::text( sanitize_text_field( self::text( $detail, 1000 ) ), 255 ), ), array( '%s', '%s', '%d', '%s', '%s', '%s' ) ); if ( $ok ) { self::prune(); } return (bool) $ok; } /** * Make text safe for a VARCHAR(n) column: drop invalid UTF-8 and cut to n characters (not bytes). * Cutting bytes can split a multibyte character, and MySQL/MariaDB then refuse the whole row. * * @param mixed $text Text. * @param int $chars Maximum characters. * @return string */ public static function text( $text, $chars ) { $text = wp_check_invalid_utf8( (string) $text, true ); return function_exists( 'mb_substr' ) ? mb_substr( $text, 0, (int) $chars, 'UTF-8' ) : substr( $text, 0, (int) $chars ); } /** * Keep only the newest MAX_ROWS rows. */ public static function prune() { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Custom table; nothing to cache. $cutoff = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i ORDER BY id DESC LIMIT 1 OFFSET %d', self::table(), self::MAX_ROWS ) ); if ( $cutoff > 0 ) { $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE id <= %d', self::table(), $cutoff ) ); } // phpcs:enable } /** * Fetch rows, newest first. * * @param int $per_page Rows per page. * @param int $page Page number (1-based). * @param string $event Optional event filter. * @return array */ public static function rows( $per_page = 50, $page = 1, $event = '' ) { global $wpdb; $offset = max( 0, ( max( 1, (int) $page ) - 1 ) * (int) $per_page ); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Custom table; nothing to cache. if ( '' !== $event ) { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE event = %s ORDER BY id DESC LIMIT %d OFFSET %d', self::table(), $event, (int) $per_page, $offset ), ARRAY_A ); } else { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i ORDER BY id DESC LIMIT %d OFFSET %d', self::table(), (int) $per_page, $offset ), ARRAY_A ); } // phpcs:enable return (array) $rows; } /** * Count rows. * * @param string $event Optional event filter. * @return int */ public static function count( $event = '' ) { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Custom table; nothing to cache. if ( '' !== $event ) { $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE event = %s', self::table(), $event ) ); } else { $count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', self::table() ) ); } // phpcs:enable return (int) $count; } /** * Delete every row. */ public static function clear() { global $wpdb; $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', self::table() ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Custom table. } /** * Rows belonging to a person (privacy export), matched by username/email and, when the email * belongs to a user account, by that user's login name and ID. * * @param string $email Email address. * @param int $per_page Rows per page. * @param int $page Page. * @return array */ public static function rows_for_person( $email, $per_page, $page ) { global $wpdb; $user = get_user_by( 'email', $email ); $name = sanitize_user( $email, false ); $offset = max( 0, ( (int) $page - 1 ) * (int) $per_page ); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Custom table; nothing to cache. if ( $user ) { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE username = %s OR username = %s OR user_id = %d ORDER BY id ASC LIMIT %d OFFSET %d', self::table(), $name, $user->user_login, (int) $user->ID, (int) $per_page, $offset ), ARRAY_A ); } else { $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE username = %s ORDER BY id ASC LIMIT %d OFFSET %d', self::table(), $name, (int) $per_page, $offset ), ARRAY_A ); } // phpcs:enable return (array) $rows; } /** * Delete rows belonging to a person (privacy erase). Same matching as rows_for_person(). * * @param string $email Email address. * @return int Rows removed. */ public static function delete_for_person( $email ) { global $wpdb; $user = get_user_by( 'email', $email ); $name = sanitize_user( $email, false ); // phpcs:disable WordPress.DB.DirectDatabaseQuery -- Custom table; nothing to cache. if ( $user ) { $removed = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE username = %s OR username = %s OR user_id = %d', self::table(), $name, $user->user_login, (int) $user->ID ) ); } else { $removed = $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE username = %s', self::table(), $name ) ); } // phpcs:enable return (int) $removed; } }