insert( Schema::table( 'audit_log' ), array( 'event' => substr( sanitize_key( $event ), 0, 60 ), 'user_id' => isset( $args['user_id'] ) ? absint( $args['user_id'] ) : 0, 'identifier' => isset( $args['identifier'] ) ? substr( sanitize_text_field( $args['identifier'] ), 0, 191 ) : '', 'ip' => substr( Request::ip(), 0, 45 ), 'user_agent' => Request::user_agent(), 'context' => $context ? wp_json_encode( $context ) : null, 'created_at' => gmdate( 'Y-m-d H:i:s' ), ), array( '%s', '%d', '%s', '%s', '%s', '%s', '%s' ) ); } /** * Returns a page of events, newest first. * * @param int $page Page number, starting at 1. * @param int $per_page Rows per page. * @param string $event Optional event slug filter. * @return array{rows: array, total: int} */ public static function query( $page = 1, $per_page = 50, $event = '' ) { global $wpdb; $table = Schema::table( 'audit_log' ); $offset = max( 0, ( absint( $page ) - 1 ) * absint( $per_page ) ); $limit = max( 1, absint( $per_page ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Custom table; table name is trusted and values are prepared. if ( '' !== $event ) { $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE event = %s", $event ) ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE event = %s ORDER BY id DESC LIMIT %d OFFSET %d", $event, $limit, $offset ) ); } else { $total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $limit, $offset ) ); } // phpcs:enable return array( 'rows' => is_array( $rows ) ? $rows : array(), 'total' => $total, ); } /** * Returns the distinct event slugs that exist in the log. * * @return string[] */ public static function event_names() { global $wpdb; $table = Schema::table( 'audit_log' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; trusted table name. $names = $wpdb->get_col( "SELECT DISTINCT event FROM {$table} ORDER BY event ASC" ); return is_array( $names ) ? $names : array(); } /** * Deletes rows older than the configured retention period. * * @return void */ public static function purge_expired() { global $wpdb; $days = (int) Options::get( 'security.audit_retention_days' ); if ( $days < 1 ) { return; } $table = Schema::table( 'audit_log' ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table; trusted table name. $wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", gmdate( 'Y-m-d H:i:s', time() - $days * DAY_IN_SECONDS ) ) ); // phpcs:enable } }