'client_ip', 'Severity' => 'severity', 'Object' => 'object', 'EventType' => 'event_type', 'UserAgent' => 'user_agent', 'CurrentUserRoles' => 'user_roles', 'Username' => 'username', 'CurrentUserID' => 'user_id', 'SessionID' => 'session_id', 'PostStatus' => 'post_status', 'PostType' => 'post_type', 'PostID' => 'post_id', ); /** * Keeps the info about the columns of the table - name, type. * * @var array * * @since 4.5.0 */ protected static $fields = array( 'id' => 'bigint', 'site_id' => 'bigint', 'alert_id' => 'bigint', 'created_on' => 'double', 'client_ip' => 'varchar(255)', 'severity' => 'varchar(255)', 'object' => 'varchar(255)', 'event_type' => 'varchar(255)', 'user_agent' => 'varchar(255)', 'user_roles' => 'varchar(255)', 'username' => 'varchar(255)', 'user_id' => 'bigint', 'session_id' => 'varchar(255)', 'post_status' => 'varchar(255)', 'post_type' => 'varchar(255)', 'post_id' => 'bigint', ); /** * Holds all the default values for the columns. * * @var array * * @since 4.6.0 */ protected static $fields_values = array( 'id' => 0, 'site_id' => 0, 'alert_id' => 0, 'created_on' => 0.0, 'client_ip' => '', 'severity' => '', 'object' => '', 'event_type' => '', 'user_agent' => '', 'user_roles' => '', 'username' => null, 'user_id' => null, 'session_id' => '', 'post_status' => '', 'post_type' => '', 'post_id' => 0, ); /** * Builds an upgrade query for the occurrence table. * * @return string */ public static function get_upgrade_query() { return 'ALTER TABLE `' . self::get_table_name() . '`' . ' DROP COLUMN is_read, ' . ' DROP COLUMN is_migrated, ' . " ADD client_ip VARCHAR(255) NOT NULL DEFAULT ''," . ' ADD severity BIGINT NOT NULL DEFAULT 0,' . " ADD object VARCHAR(255) NOT NULL DEFAULT ''," . " ADD event_type VARCHAR(255) NOT NULL DEFAULT ''," . " ADD user_agent VARCHAR(255) NOT NULL DEFAULT ''," . " ADD user_roles VARCHAR(255) NOT NULL DEFAULT ''," . ' ADD username VARCHAR(255) NULL,' . ' ADD user_id BIGINT NULL ,' . " ADD session_id VARCHAR(255) NOT NULL DEFAULT ''," . " ADD post_status VARCHAR(255) NOT NULL DEFAULT ''," . " ADD post_type VARCHAR(255) NOT NULL DEFAULT ''," . ' ADD post_id BIGINT NOT NULL DEFAULT 0;'; } /** * Creates table functionality * * @param \wpdb $connection - \wpdn connection to be used for name extraction. * * @return bool * * @since 4.4.2.1 * @since 4.6.0 - Added $connection parameter */ public static function create_table( $connection = null ): bool { $collate = self::get_connection()->get_charset_collate(); if ( null !== $connection ) { if ( $connection instanceof \wpdb ) { $collate = $connection->get_charset_collate(); } } $table_name = self::get_table_name( $connection ); $wp_entity_sql = ' CREATE TABLE `' . $table_name . '` ( `id` bigint NOT NULL AUTO_INCREMENT, `site_id` bigint NOT NULL, `alert_id` bigint NOT NULL, `created_on` double NOT NULL, `client_ip` varchar(255) NOT NULL, `severity` varchar(255) NOT NULL, `object` varchar(255) NOT NULL, `event_type` varchar(255) NOT NULL, `user_agent` varchar(255) NOT NULL, `user_roles` varchar(255) NOT NULL, `username` varchar(255) DEFAULT NULL, `user_id` bigint DEFAULT NULL, `session_id` varchar(255) NOT NULL, `post_status` varchar(255) NOT NULL, `post_type` varchar(255) NOT NULL, `post_id` bigint NOT NULL, PRIMARY KEY (`id`), KEY `site_alert_created` (`site_id`,`alert_id`,`created_on`), KEY `created_on` (`created_on`) ) ' . $collate . ';'; return self::maybe_create_table( $table_name, $wp_entity_sql, $connection ); } /** * Responsible for storing the information in both occurrences table and metadata table. * That one is optimized for DB performance * * @param array $data - The data to be stored. * @param int $type - The event ID. * @param float $date - Formatted to UNIX timestamp date. * @param int $site_id - The site ID to store data for. * * @return void * * @since 4.5.0 */ public static function store_record( $data, $type, $date, $site_id ) { $data_to_store = array(); foreach ( (array) $data as $name => $value ) { if ( '0' === $value || ! empty( $value ) ) { if ( isset( self::$migrated_meta[ $name ] ) ) { if ( 'CurrentUserRoles' === $name ) { $value = maybe_unserialize( $value ); if ( is_array( $value ) && ! empty( $value ) ) { $data_to_store[ self::$migrated_meta[ $name ] ] = implode( ',', $value ); } elseif ( ! empty( $value ) ) { $data_to_store[ self::$migrated_meta[ $name ] ] = $value; } } else { $data_to_store[ self::$migrated_meta[ $name ] ] = $value; } unset( $data[ $name ] ); } } if ( 'CurrentUserID' === $name && ! \is_object( $value ) && 0 === (int) $value ) { $data_to_store[ self::$migrated_meta[ $name ] ] = $value; } } if ( ! empty( $data_to_store ) ) { $data_to_store['created_on'] = $date; $data_to_store['alert_id'] = $type; $data_to_store['site_id'] = ! is_null( $site_id ) ? $site_id : ( function_exists( 'get_current_blog_id' ) ? get_current_blog_id() : 0 ); if ( in_array( $type, array( 1000, 1001, 1002, 1003 ) ) ) { if ( empty( $data_to_store['user_id'] ) && empty( $data_to_store['username'] ) ) { $data_to_store['user_id'] = 0; $data_to_store['username'] = 'Unknown user'; } elseif ( empty( $data_to_store['username'] ) ) { if ( 0 === (int) $data_to_store['user_id'] ) { $data_to_store['username'] = 'Unknown User'; } else { $user = \get_user_by( 'ID', $data_to_store['user_id'] ); if ( $user ) { $data_to_store['username'] = $user->user_login; } else { $data_to_store['username'] = 'Deleted'; } } } elseif ( empty( (int) $data_to_store['user_id'] ) ) { if ( 0 === (int) $data_to_store['user_id'] ) { $data_to_store['username'] = 'Unknown User'; } else { $user = \get_user_by( 'login', $data_to_store['username'] ); if ( $user ) { $data_to_store['user_id'] = $user->ID; } else { $data_to_store['user_id'] = 0; } } } } $occurrences_id = self::save( $data_to_store ); if ( 0 !== $occurrences_id && ! empty( $data ) ) { $sqls = ''; foreach ( (array) $data as $name => $value ) { $meta_insert = array( 'occurrence_id' => $occurrences_id, 'name' => $name, 'value' => maybe_serialize( $value ), ); $data_prepared = Metadata_Entity::prepare_data( $meta_insert ); $fields = '`' . implode( '`, `', array_keys( $data_prepared[0] ) ) . '`'; $formats = implode( ', ', $data_prepared[1] ); $sql = "($formats),"; $sqls .= self::get_connection()->prepare( $sql, $data_prepared[0] ); } if ( ! empty( $sqls ) ) { $sqls = 'INSERT INTO `' . Metadata_Entity::get_table_name() . "` ($fields) VALUES " . rtrim( $sqls, ',' ); self::get_connection()->suppress_errors( true ); self::get_connection()->query( $sqls ); if ( '' !== self::get_connection()->last_error ) { if ( 1146 === Metadata_Entity::get_last_sql_error( self::get_connection() ) ) { if ( Metadata_Entity::create_table() ) { self::get_connection()->query( $sqls ); } } } self::get_connection()->suppress_errors( false ); } } } } /** * Sets an index (if not there already) * * @param \wpdb $connection - \wpdb connection to be used for name extraction. * * @return void * * @since 4.5.1 * @since 4.6.0 - Added connection parameter */ public static function create_indexes( $connection = null ) { $index_exists = false; $db_connection = self::get_connection(); if ( null !== $connection ) { if ( $connection instanceof \wpdb ) { $db_connection = $connection; } } // check if an index exists. if ( $db_connection->query( 'SELECT COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema=DATABASE() AND table_name="' . self::get_table_name( $connection ) . '" AND index_name="created_on"' ) ) { // query succeeded, does index exist? $index_exists = ( isset( $db_connection->last_result[0]->IndexIsThere ) ) ? $db_connection->last_result[0]->IndexIsThere : false; } // if no index exists then make one. if ( ! $index_exists ) { $db_connection->query( 'CREATE INDEX created_on ON ' . self::get_table_name( $connection ) . ' (created_on)' ); } } /** * Gets alert message. * * @param array $item - Occurrence meta array. * @param string $context Message context. * * @return string Full-formatted message. * @see WSAL_Alert::get_message() * * @since 4.6.0 */ public static function get_alert_message( $item = null, $context = 'default' ) { // $alert = Alert_Manager::get_alert( // $item['alert_id'], // (object) array( // 'mesg' => esc_html__( 'Alert message not found.', 'wp-security-audit-log' ), // 'desc' => esc_html__( 'Alert description not found.', 'wp-security-audit-log' ), // ) // ); // $cached_message = $alert->mesg; if ( ! isset( $item['meta_values'] ) ) { return ''; } // Fill variables in message. $meta_array = $item['meta_values']; // $alert_object = $alert; $message = Alert::get_message( $meta_array, null, $item['alert_id'], $item['id'], $context ); if ( false === $message ) { $cached_message = Alert::get_original_alert_message( $item['alert_id'] ); } if ( false !== $message ) { $cached_message = $message; } else { $cached_message = isset( $cached_message ) ? $cached_message : sprintf( /* Translators: 1: html that opens a link, 2: html that closes a link. */ __( 'This type of activity / change is no longer monitored. You can create your own custom event IDs to keep a log of such change. Read more about custom events %1$shere%2$s.', 'wp-security-audit-log' ), '', '' ); } return $cached_message; } /** * Gets alert meta. * * @param array $item - Occurrence meta array. * * @return string Raw meta stored. * * @since 5.1.0 */ public static function get_alert_meta( $item = \null ) { if ( ! isset( $item['meta_values'] ) ) { return ''; } // Fill variables in message. $meta_array = $item['meta_values']; $cached_message = ''; foreach ( $meta_array as $name => $value ) { if ( \is_array( $value ) || is_object( $value ) ) { $value = \print_r( (array) $value, \true ); } $cached_message .= $name . ' : ' . $value . '
'; } return $cached_message; } /** * Retrieves occurrences that have metadata that needs to be migrated to the occurrences table. This relates to the * database schema change done in version 4.4.0. * * @param int $limit Limits the number of results. * * @return WSAL_Models_Occurrence[] * * @since 4.6.0 */ public static function get_all_with_meta_to_migrate( $limit ) { $meta_keys = array_map( function ( $value ) { return '"' . $value . '"'; }, array_keys( self::$migrated_meta ) ); return self::load_multi_query( 'SELECT o.* FROM `' . self::get_table_name() . '` o ' . ' INNER JOIN `' . Metadata_Entity::get_table_name() . '` m ' . ' ON m.occurrence_id = o.id ' . ' WHERE m.name IN (' . implode( ',', $meta_keys ) . ') ' . ' GROUP BY o.id ' . ' ORDER BY created_on DESC ' . ' LIMIT 0, %d;', array( $limit ) ); } /** * Returns the value of a meta item. * * @param array $data_collected - The array with all of the items, must contain at least the id of the occurrence. * @param string $name - Name of meta item. * @param mixed $default - Default value returned when meta does not exist. * * @return mixed The value, if meta item does not exist $default returned. * @see WSAL_Adapters_MySQL_Occurrence::get_named_meta() * * @throws \Exception - if the id is not part of the $data_collected array. * * @since 4.6.0 */ public static function get_meta_value( array $data_collected, $name, $default = array() ) { $result = $default; if ( ! isset( $data_collected['id'] ) ) { throw new \Exception( 'The id of the Occurrence is not present in the array', 1 ); } // Check if the meta is part of the occurrences table. if ( in_array( $name, array_keys( self::$migrated_meta ), true ) ) { $property_name = self::$migrated_meta[ $name ]; if ( isset( $data_collected[ $property_name ] ) ) { $result = $data_collected[ $property_name ]; } } else { // Get meta adapter. $meta = Metadata_Entity::load_by_name_and_occurrence_id( $name, $data_collected['id'] ); if ( is_null( $meta ) || ! array_key_exists( 'value', $meta ) ) { return $default; } $result = $meta['value']; } $result = maybe_unserialize( $result ); if ( 'CurrentUserRoles' === $name && is_string( $result ) ) { $result = preg_replace( '/[\[\]"]/', '', $result ); $result = explode( ',', $result ); } return $result; } /** * Saves the given data into the table * The data should be in following format: * field name => value * * It checks the given data array against the table fields and determines the types based on that, it stores the values in the table then. * * @param array $data - The data to be saved (check above about the format). * * @return int * * @since 4.6.0 */ public static function save( $data ) { // Use today's date if not set up. if ( ! isset( $data['created_on'] ) ) { $data['created_on'] = microtime( true ); } return parent::save( $data ); } /** * Returns a key-value pair of metadata. * * @param int $occurrence_id - The ID of the occurrence to extract data from. * @param array $record_data - Array with the already prepared event data. * @param \wpdb $connection - \wpdb connection to be used for name extraction. * * @return array * * @since 4.6.0 */ public static function get_meta_array( int $occurrence_id, array $record_data = array(), $connection = null ) { $result = array(); if ( null !== $connection ) { if ( $connection instanceof \wpdb ) { $_wpdb = $connection; } } else { $_wpdb = self::get_connection(); } $metas = Metadata_Entity::load_array( 'occurrence_id = %d', array( $occurrence_id ), $_wpdb ); foreach ( $metas as $meta ) { $result[ $meta['name'] ] = maybe_unserialize( $meta['value'] ); } if ( empty( $record_data ) ) { $record_data = self::load_array( 'id = %d', array( $occurrence_id ), $_wpdb ); $record_data = \reset( $record_data ); } if ( isset( $record_data ) && ! empty( $record_data ) ) { foreach ( self::$migrated_meta as $meta_key => $column_name ) { $result[ $meta_key ] = $record_data[ $column_name ]; } } return $result; } /** * Returns a key-value pair of metadata. * * @param array $events - Array with all the events to extract meta data for. * @param \wpdb $connection - \wpdb connection to be used for name extraction. * * @return array * * @since 4.6.0 */ public static function get_multi_meta_array( array &$events, $connection = null ) { $event_ids = array(); foreach ( $events as &$event ) { if ( ! isset( $event['id'] ) ) { $events = array(); return $events; } $event_ids[] = (int) $event['id']; } unset( $event ); $events = \array_combine( $event_ids, $events ); if ( null !== $connection ) { if ( $connection instanceof \wpdb ) { $_wpdb = $connection; } } else { $_wpdb = self::get_connection(); } $metas = Metadata_Entity::load_by_occurrences_ids( $event_ids, $_wpdb ); foreach ( $metas as $meta ) { $events[ $meta['occurrence_id'] ]['meta_values'][ $meta['name'] ] = maybe_unserialize( $meta['value'] ); } foreach ( $events as &$event ) { foreach ( self::$migrated_meta as $meta_key => $column_name ) { $event['meta_values'][ $meta_key ] = $event[ $column_name ]; } } unset( $event ); return $events; } /** * Get distinct values of IPs. * * @param int $limit - (Optional) Limit. * * @return array - Distinct values of IPs. * * @since 4.6.0 */ public static function get_matching_ips( $limit = null ) { $_wpdb = self::get_connection(); $sql = 'SELECT DISTINCT client_ip FROM ' . self::get_table_name(); if ( ! is_null( $limit ) ) { $sql .= ' LIMIT ' . $limit; } $ips = $_wpdb->get_col( $sql ); $result = array(); foreach ( $ips as $ip ) { if ( 0 === strlen( trim( (string) $ip ) ) ) { continue; } array_push( $result, $ip ); } $_wpdb = null; return array_unique( $result ); } /** * Search IPs for a specific ip text search * * @param string $search - The IP search string. * @param int|string $limit - The limit results. * * @return array * * @since 5.0.0 */ public static function get_ips_logged_search( string $search = '', $limit = null ) { $_wpdb = self::get_connection(); $sql = 'SELECT DISTINCT client_ip FROM ' . self::get_table_name(); if ( ! empty( $search ) ) { $sql .= ' WHERE 1 AND client_ip LIKE "%' . $_wpdb->esc_like( $search ) . '%"'; } if ( ! is_null( $limit ) ) { $sql .= ' LIMIT ' . $limit; } $ips = $_wpdb->get_col( $sql ); $result = array(); foreach ( $ips as $ip ) { if ( 0 === strlen( trim( (string) $ip ) ) ) { continue; } array_push( $result, $ip ); } return $result; } /** * Collects and prepares all the metadata for the given array of the events. * * @param array $results - Array with all the data with events collected. * * @return array * * @since 4.6.0 */ public static function prepare_with_meta_data( array &$results ): array { $prepared_array = array(); $table_name = self::get_table_name(); $meta_table_name = Metadata_Entity::get_table_name(); if ( is_array( reset( $results[0] ) ) || ( is_array( $results[0] ) && ! empty( $results[0] ) && ! isset( $results[0][0] ) ) ) { foreach ( $results as $result_row ) { if ( ! isset( $prepared_array[ $result_row[ $table_name . 'id' ] ] ) ) { foreach ( array_keys( self::$fields ) as $field ) { $prepared_array[ $result_row[ $table_name . 'id' ] ][ $field ] = $result_row[ $table_name . $field ]; $prepared_array[ $result_row[ $table_name . 'id' ] ][ $field ] = self::cast_to_correct_type( $field, $prepared_array[ $result_row[ $table_name . 'id' ] ][ $field ] ); } foreach ( self::$migrated_meta as $name => $new_name ) { $prepared_array[ $result_row[ $table_name . 'id' ] ]['meta_values'][ $name ] = \maybe_unserialize( $result_row[ $table_name . $new_name ] ); } } $prepared_array[ $result_row[ $table_name . 'id' ] ]['meta_values'][ $result_row[ $meta_table_name . 'name' ] ] = \maybe_unserialize( $result_row[ $meta_table_name . 'value' ] ); } } return $prepared_array; } /** * Executes and returns the result of the given query. * * @param string $sql - The SQL query to execute. * @param \wpdb $connection - The database connection. * * @return array * * @since 5.0.0 */ public static function load_query( string $sql, $connection = null ): array { if ( null !== $connection ) { if ( $connection instanceof \wpdb ) { $_wpdb = $connection; } } else { $_wpdb = self::get_connection(); } $_wpdb->suppress_errors( true ); $res = $_wpdb->get_results( $sql, ARRAY_A ); if ( '' !== $_wpdb->last_error ) { if ( 1146 === self::get_last_sql_error( $_wpdb ) ) { if ( ( static::class )::create_table( $_wpdb ) ) { $res = $_wpdb->get_results( $sql, ARRAY_A ); } } } $_wpdb->suppress_errors( false ); return $res; } /** * Cleans up the database, based on pruning date selected and enabled. * * @return void * * @since 5.0.0 */ public static function prune_records() { $prune_enabled = Settings_Helper::get_boolean_option_value( 'pruning-date-e', false ); if ( ! $prune_enabled ) { return; } $now = time(); $max_sdate = Plugin_Settings_Helper::get_pruning_date(); // Pruning date. $archiving = Settings_Helper::is_archiving_set_and_enabled(); // phpcs:disable // phpcs:enable // Calculate limit timestamp. $max_stamp = $now - ( strtotime( $max_sdate ) - $now ); $items = array(); if ( $archiving ) { $connection_name = Settings_Helper::get_option_value( 'archive-connection' ); $wsal_db = Connection::get_connection( $connection_name ); $items = Delete_Records::delete( array(), 0, array( 'created_on <= %s' => intval( $max_stamp ) ), $wsal_db ); } $main_items = Delete_Records::delete( array(), 0, array( 'created_on <= %s' => intval( $max_stamp ) ) ); } } }