AlkantarClanX12

Your IP : 216.73.217.24


Current Path : /www/capitalgmcbuickregina_830/public/wp-content/plugins/wp-cerber/includes/
Upload File :
Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/wp-cerber/includes/CRB_Activity.php

<?php
/*
	Copyright (C) 2015-26 CERBER TECH INC., https://wpcerber.com

    Licensed under the GNU GPL.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

*/

/*

*========================================================================*
|                                                                        |
|	       ATTENTION!  Do not change or edit this file!                  |
|                                                                        |
*========================================================================*

*/

/**
 * Owns the application-level Activity Log boundary.
 *
 * This class is the central service for recording security-relevant activity
 * events, querying them for administrative screens, and streaming them for export.
 * It keeps activity-log concerns behind one stable service boundary so callers do
 * not need to know the log table shape, query filter semantics, cache invalidation
 * rules, or export query strategy. Nearby components provide the lower-level work:
 * DB Warp executes the database operations, CRB_Activity_Alerts reacts to newly
 * recorded events, and the administrative UI consumes the filtered reads and export
 * streams produced here.
 *
 * Its expected side effects are deliberate and bounded to the activity subsystem:
 * writing and deleting activity rows, refreshing log-status cache metadata,
 * dispatching configured activity alerts, and forwarding selected events to the Lab
 * integration.
 *
 * @since 9.6.6.10
 */
final class CRB_Activity {
	/**
	 * Activity IDs logged during the current HTTP request.
	 *
	 * @var array<int,int>
	 */
	private static array $logged = array();

	/**
	 * Activity and user logged during the current HTTP request for log deduplication.
	 *
	 * @var array<string,bool>
	 */
	private static array $dedup = array();

	/**
	 * The list of activities not to log
	 *
	 * @var array<int>
	 */
	private static array $ignore = array();

	// HTTP query string parameters with mapping
	// Parameter name => DB table field, if direct mapping is applicable

	const EVENT_FILTERING_PARAMS = array(
		'filter_activity'   => 'activity',
		'filter_status'     => 'ac_status',
		'filter_set'        => false,
		'filter_ip'         => 'ip',
		'filter_login'      => 'user_login',
		'filter_user'       => 'user_id',
		'search_activity'   => false,
		'filter_sid'        => 'session_id',
		'search_url'        => false,
		'filter_country'    => 'country',
		'filter_time_begin' => false,
		'filter_time_end'   => false,
	);

	/**
	 * Storage key for caching database table status.
	 *
	 */
	const LOG_TABLE_STATUS = 'activity_table_status';

	/**
	 * Get records from the activity log using specified conditions
	 *
	 * @param array $activity List of activity IDs.
	 * @param array $user User parameters ('email' or 'id').
	 * @param array $order Order parameters ('DESC' => 'column_name' or 'ASC' => 'column_name').
	 * @param string $limit SQL LIMIT clause. Example: '10' or '0, 20'
	 *
	 * @return object[] Array of log record objects. An empty array if no records are found.
	 */
	public static function get_log( array $activity = [], array $user = [], $order = [], $limit = '' ): array {

		// Resolve the optional user filter to a numeric ID before touching the database.
		$user_id = 0;

		if ( ! empty( $user['email'] ) ) {
			$user_obj = get_user_by( 'email', $user['email'] );

			if ( ! $user_obj ) {
				return array();
			}

			$user_id = absint( $user_obj->ID );
		}
		elseif ( ! empty( $user['id'] ) ) {
			$user_id = absint( $user['id'] );
		}

		// Build and run the read through the centralized query builder.
		$db_result = warp_get_db();

		if ( $db_result->has_errors() ) {
			return array();
		}

		/** @var CRB_Database $db */
		$db = $db_result->get_results();
		$qb = $db->table( CERBER_LOG_TABLE );

		if ( $activity ) {
			$qb->where( 'activity', 'IN', array_map( 'absint', $activity ) );
		}

		if ( $user_id ) {
			$qb->where( 'user_id', '=', $user_id );
		}

		// Ordering, restricted to the supported DESC/ASC directions.
		if ( $order ) {
			$order_key = key( $order );
			$order_column = crb_sanitize_id( current( $order ) );

			// Sanitize the column so an unusable value degrades to "no ordering"
			// instead of turning the whole read into a builder error that would
			// zero the result set.
			if ( in_array( $order_key, [ 'DESC', 'ASC' ], true ) && $order_column !== '' ) {
				$qb->order_by( $order_column, $order_key );
			}
		}

		// Pagination expressed as 'count' or 'offset,count'.
		if ( $limit ) {
			$clean = preg_replace( '/[^0-9,]/', '', (string) $limit );

			if ( strpos( $clean, ',' ) !== false ) {
				list( $offset_part, $count_part ) = explode( ',', $clean, 2 );
				$qb->limit( (int) $count_part )->offset( (int) $offset_part );
			}
			elseif ( $clean !== '' ) {
				$qb->limit( (int) $clean );
			}
		}

		$result = $qb->get_query_results( CRB_Database::FETCH_OBJECTS );

		if ( $result->has_errors() ) {
			return array();
		}

		return $result->get_results( array() ) ?: array();
	}

	/**
	 * Log an activity event into the activity log.
	 *
	 * @param int $activity Activity ID.
	 * @param string $login Login used or any additional information. Empty by default.
	 * @param int $user_id User ID; defaults to the current user when zero.
	 * @param int $status Activity status; resolved automatically when zero.
	 * @param string $ip Remote IP address; auto-detected when empty.
	 *
	 * @return bool True when the event row was written. False when the event was
	 *              skipped (duplicate or ignored), the IP address is invalid, or
	 *              the database write failed.
	 *
	 * @since 3.0
	 */
	public static function log( int $activity, string $login = '', int $user_id = 0, int $status = 0, string $ip = '' ): bool {
		global $user_ID;

		$activity = absint( $activity );

		if ( empty( $user_id ) ) {
			$user_id = ( $user_ID ) ?: 0;
		}

		$user_id = absint( $user_id );

		$key = $activity . '-' . $user_id;

		if ( ( isset( self::$dedup[ $key ] )
		       || isset( self::$ignore[ $activity ] ) )
		     && ! defined( 'CRB_ALLOW_MULTIPLE' ) ) {
			return false;
		}

		self::$dedup[ $key ] = true;
		self::$logged[ $activity ] = $activity;

		// Assemble and normalize the event; an empty result signals an invalid IP

		$event = self::build_event( $activity, $login, $user_id, $status, $ip );

		if ( ! $event ) {
			return false;
		}

		$log_row = $event['row'];

		// Persist the event to the activity log

		$ret = self::persist_event( $log_row );

		// If alerts exist, lazy-load the alert class and dispatch a notification

		if ( cerber_get_site_option( CRB_ALL_ALERTS ) ) {
			CRB_Activity_Alerts::dispatch( array(
				'activity'   => $log_row['activity'],
				'status'     => $log_row['ac_status'],
				'user_id'    => $log_row['user_id'],
				'ac_by_user' => $log_row['ac_by_user'],
				'ip'         => $log_row['ip'],
				'ip_long'    => $log_row['ip_long'],
				'login'      => $log_row['user_login'],
				'url'        => $event['url'],
				'country'    => $log_row['country'],
			) );
		}

		// Lab --------------------------------------------------------------------

		if ( in_array( $log_row['activity'], array( CRB_EV_CMS, CRB_EV_SCD, CRB_EV_SFD, 40, CRB_EV_PUR, CRB_EV_LDN, 55, 56, 71 ) ) ) {
			lab_save_push( $log_row['ip'], $log_row['activity'] );
		}

		return $ret;
	}

	/**
	 * Assemble and normalize a single activity event from the raw log arguments.
	 *
	 * Resolves and validates the IP address, derives all event fields (IP long,
	 * IPv6 /64 network, timestamp, request URL, status, country, session ID and
	 * the control details string) and packs the database row. The method has no
	 * side effects: its result is consumed by persist_event() and the alert
	 * dispatcher.
	 *
	 * @param int $activity Activity ID, already normalized by the caller.
	 * @param string $login Login used or any additional information.
	 * @param int $user_id User ID, already normalized by the caller.
	 * @param int $status Activity status; resolved here when zero.
	 * @param string $ip Remote IP address; auto-detected when empty.
	 *
	 * @return array{row:array<string,mixed>,url:string}|array{} The assembled event, or an empty array when the supplied IP address is invalid.
	 */
	private static function build_event( int $activity, string $login, int $user_id, int $status, string $ip ): array {
		$wp_cerber = get_wp_cerber();

		// Resolve the IP address: auto-detect when empty, reject when malformed

		if ( empty( $ip ) ) {
			$ip = cerber_get_remote_ip();
		}
		elseif ( ! filter_var( $ip, FILTER_VALIDATE_IP ) ) {
			return array();
		}

		if ( cerber_is_ipv4( $ip ) ) {
			$ip_long = ip2long( $ip );
		}
		else {
			$ip_long = 1;
		}

		$ipv6_net_64 = crb_pack_ipv6_net64( $ip );

		$stamp = microtime( true );

		// Build the request URL from the host and the path without the query string

		$request_uri = $_SERVER['REQUEST_URI'] ?? '';
		$query_position = strpos( $request_uri, '?' );
		$request_path = ( $query_position !== false )
			? substr( $request_uri, 0, $query_position )
			: $request_uri;

		// Remove non-ASCII = robustness
		$request_path = preg_replace( '/[^\x20-\x7E]/', '', (string) $request_path );

		$http_host = $_SERVER['HTTP_HOST'] ?? '';
		$http_host = preg_replace( '/[^a-z0-9.\-:\[\]]/i', '', (string) $http_host );

		$url = $http_host . $request_path;

		// Resolve the status when the caller did not provide one

		if ( ! $status ) {
			if ( $activity != CRB_EV_IPL && $activity != CRB_EV_NEL ) {
				$status = cerber_get_status( $ip, $activity );
			}
			elseif ( CRB_Globals::$blocked ) {
				$status = CRB_Globals::$blocked;
			}
		}

		$ac_bot = absint( CRB_Globals::$bot_status );
		$ac_by_user = absint( CRB_Globals::$user_id );

		$ctrl_txt = '';

		if ( $ctrl = CRB_Globals::get_ctrl_settings() ) {
			$ctrl_txt = implode( ',', $ctrl ); // @since 9.6.1.1
		}

		$details = $ctrl_txt . '|0|0|0|' . $url;
		$details = substr( $details, 0, 250 );

		$country = (string) lab_get_country( $ip );
		$status = absint( $status ); // Note: @since 8.9.4 $status is stored in a separate "ac_status" column
		$session_id = $wp_cerber->getRequestID();

		$log_row = array(
			'ip'          => $ip,
			'ip_long'     => $ip_long,
			'ipv6_net_64' => $ipv6_net_64,
			'user_login'  => $login,
			'user_id'     => $user_id,
			'stamp'       => $stamp,
			'activity'    => $activity,
			'session_id'  => $session_id,
			'country'     => $country,
			'details'     => $details,
			'ac_status'   => $status,
			'ac_bot'      => $ac_bot,
			'ac_by_user'  => $ac_by_user,
		);

		return array( 'row' => $log_row, 'url' => $url );
	}

	/**
	 * Insert an assembled event row into the activity log table.
	 *
	 * Performs the database write through the DB Warp layer (parameterized
	 * insert) and refreshes the activity status cache on success. The supplied
	 * row is trusted as already normalized by build_event().
	 *
	 * @param array<string,mixed> $log_row Database row keyed by column name.
	 *
	 * @return bool True when the row was written, false on a database error.
	 */
	private static function persist_event( array $log_row ): bool {
		$db_result = warp_get_db();
		$write = $db_result->has_errors()
			? $db_result
			: $db_result->get_results()->table( CERBER_LOG_TABLE )->insert( $log_row );

		if ( $write->has_errors() ) {
			cerber_watchdog();

			return false;
		}

		self::set_status( array( 'data_modified' => $log_row['stamp'], 'hash' => sha1( $log_row['ip'] . $log_row['stamp'] . $log_row['session_id'] ) ) );

		return true;
	}

	/**
	 * Do not log this activity
	 *
	 * @param int $activity Activity ID
	 *
	 * @return void
	 */
	public static function set_ignore( int $activity ) {
		self::$ignore[ $activity ] = true;
	}

	/**
	 * Check if the given activities were logged during the current HTTP request
	 *
	 * @param int|int[] $activity ID of activities to check for.
	 *
	 * @return boolean
	 *
	 * @since 9.5.8
	 */
	public static function is_logged( $activity ) {
		if ( ! self::$logged ) {
			return false;
		}

		if ( ! is_array( $activity ) ) {
			$activity = array( $activity );
		}

		return ! empty( array_intersect( array_map( 'absint', $activity ), self::$logged ) );
	}

	/**
	 * Returns the list of activities logged during the current HTTP request
	 *
	 * @return array
	 *
	 * @since 9.6.6.10
	 */
	static function get_logged(): array {
		return self::$logged;
	}

	/**
	 * Is log empty?
	 *
	 * @return bool True if the activity log is empty (nothing is logged)
	 *
	 * @since 9.6.6.11
	 */
	static function is_empty(): bool {
		if ( self::get_status() ) {
			return false;
		}

		// cerber_db_is_empty()

		$db_result = warp_get_db();
		$probe = $db_result->has_errors()
			? $db_result
			: $db_result->get_results()->table( CERBER_LOG_TABLE )->select( 'ip' )->limit( 1 )->get_value();

		// It is safer to treat the log as non-empty if a DB error occurs.
		if ( $probe->get_results() || $probe->has_errors() ) {
			self::set_status( [ 'non_empty' => time() ] );

			return false;
		}

		//self::set_status( [ 'non_empty' => 0 ] );

		return true;
	}

	/**
	 * Get global log status data
	 *
	 * @return array
	 *
	 * @since 9.6.6.11
	 */
	private static function get_status(): array {
		$status = cerber_cache_get( self::LOG_TABLE_STATUS );

		if ( ! $status
		     || ! is_array( $status ) ) {
			$status = array();
		}

		return $status;
	}

	/**
	 * Set global log status data
	 *
	 * @param array $data Properties to set: ['name' => value]
	 *
	 * @return bool
	 *
	 * @since 9.6.6.11
	 */
	private static function set_status( array $data ): bool {

		$status = array_merge( self::get_status(), $data );

		return cerber_cache_set( self::LOG_TABLE_STATUS, $status );
	}

	/**
	 * Reset activity log status information
	 *
	 * @return bool
	 *
	 * @since 9.6.6.12
	 */
	static function clear_status(): bool {
		return cerber_cache_set( self::LOG_TABLE_STATUS, [] );
	}

	/**
	 * Checks if the activity log has been modified since the last log update info stored in $status.
	 *
	 * @param array $status Data returned by self::get_status()
	 *
	 * @return bool True if the log has been modified since the last log update info stored in $status
	 *
	 * @see self::get_status() Use to save the status of the activity log for further comparing.
	 *
	 * @since 9.6.6.11
	 */
	static function is_modified( array $status ): bool {
		$current = self::get_status();

		if ( empty( $current['data_modified'] )
		     || empty( $status['data_modified'] ) ) {
			return true;
		}

		return ( $current['data_modified'] > $status['data_modified'] );
	}

	/**
	 * Checks if the activity log table has been modified since the specified time.
	 * It can be used for cache invalidation.
	 *
	 * @param float|int $stamp Unix timestamp
	 *
	 * @return bool True if modified
	 *
	 * @since 9.6.6.11
	 */
	static function is_modified_since( $stamp ): bool {
		if ( ! $status = self::get_status() ) {
			return true;
		}

		return ( $stamp < ( $status['data_modified'] ?? PHP_INT_MAX ) );
	}

	/**
	 * Retrieves log rows as an array of objects, using data from cache
	 * if the log was not updated from the previous invocation
	 *
	 * If the data is not in the cache, or if the cached data is outdated (based on the global log's
	 * modification status), the builder executes the query against the database, and
	 * the results are cached.
	 *
	 * @param CRB_Query_Builder $qb Configured query builder that compiles and executes the activity log read.
	 * @param int|null &$num The number of rows in the resulting dataset. Passed by reference.
	 *
	 * @return array<object> An array of objects representing the log rows.
	 *                            An empty array if no rows are found or a database error occurs.
	 *
	 * @since 9.6.6.11
	 */
	private static function get_rows( CRB_Query_Builder $qb, ?int &$num = null ): array {

		$calculate_count = func_num_args() > 1; // We have to distinct calls with and without parameter $num

		$key = 'ac_cache_' . sha1( $qb->to_sql() . '/' . ( $calculate_count ? 'A' : 'B' ) );

		if ( ! ( $cache = cerber_cache_get( $key ) )
		     || self::is_modified( $cache['status'] ?? [] ) ) {

			$cache = array();

			// Execute the read through the builder's own terminal methods. The builder owns its
			// CRB_Database connection, so query construction and execution stay on one layer.

			$rows = array();
			$found_rows = 0;
			$query_result = $qb->get_query_results( CRB_Database::FETCH_OBJECTS );

			if ( ! $query_result->has_errors() ) {
				$rows = $query_result->get_results( array() );

				// FOUND_ROWS() reports the total recorded by the SQL_CALC_FOUND_ROWS SELECT that
				// get_query_results() just ran on the same connection. Read it through the builder's
				// companion only when the caller asked for the total and the query returned rows. A
				// count failure is non-critical and degrades to zero, matching the previous behavior.

				if ( $rows && $calculate_count ) {
					$found_result = $qb->get_found_rows();

					if ( ! $found_result->has_errors() ) {
						$found_rows = (int) $found_result->get_results( 0 );
					}
				}
			}

			// Preserve the previous contract: an empty result set (no match or a database
			// error) reports a zero total, a populated result without a count request reports
			// false, otherwise the FOUND_ROWS() total.

			if ( ! $rows ) {
				$cache['rows'] = array();
				$cache['num'] = 0;
			}
			else {
				$cache['rows'] = $rows;
				$cache['num'] = $calculate_count ? $found_rows : false;
			}

			$cache['status'] = self::get_status();

			cerber_cache_set( $key, $cache, 24 * 3600 );
		}

		$num = $cache['num'];

		return $cache['rows'];
	}

	/**
	 * Retrieves activity log rows using the request filters.
	 *
	 * Builds the SQL internally from the given arguments and executes it through the
	 * cached reader.
	 *
	 * On success the payload carries:
	 *  - 'rows'    an array of raw activity log row objects (stdClass); empty when nothing matches;
	 *  - 'total'   the total number of matching rows for pagination (SQL_CALC_FOUND_ROWS), or 0 when
	 *              pagination navigation is not required;
	 *  - 'filters' the echo-back filter values for rendering the filter form.
	 *
	 * @param array $args Optional arguments to use instead of the HTTP query string.
	 *
	 * @return Revalt<array{rows:array<object>,total:int,filters:array{per_page:int,falist:array<int>,ip:string,login:string,user_id:int,search:string,sid:string,in_url:string,country:string,time_begin:int,time_end:int}}>
	 *         Success carries the read payload described above. On failure the payload is absent and
	 *         the lower-layer Revalt is chained for root-cause diagnostics.
	 *
	 * @since 9.8.1
	 */
	public static function fetch( array $args = array() ): Revalt {

		$parsed = self::parse_query( $args );

		// A failed build aborts before any read. The parser Revalt is chained for diagnostics so the
		// caller can render the filter form from its own default-shaped values.

		if ( $parsed->has_errors() ) {
			return new Revalt( null, 'activity_fetch_query_build_failed', 'Activity fetch aborted: unable to build the activity query', null, $parsed );
		}

		$built = $parsed->get_results();

		$query_builder = $built['builder'];

		// Calculate the total count only when pagination navigation is required.

		$total = 0;

		if ( ! empty( $built['calc_on'] ) ) {
			$rows = self::get_rows( $query_builder, $total );
		}
		else {
			$rows = self::get_rows( $query_builder );
		}

		return new Revalt( array(
			'rows'    => $rows,
			'total'   => $total,
			'filters' => $built['filters'],
		) );
	}

	/**
	 * Builds the activity export query and opens an unbuffered row stream for CSV export.
	 * The stream is a generator that yields one raw activity row object (stdClass) per iteration.
	 *
	 * Resolves the export metadata eagerly while the connection is still free: the header total
	 * runs as a separate buffered COUNT query and the date range as a buffered MIN/MAX query,
	 * because the unbuffered row stream locks the connection while it is consumed.
	 *
	 * On success the payload carries:
	 *  - 'stream'  a Generator yielding raw activity row objects, never wrapped into Revalt;
	 *  - 'total'   the total number of matching rows for the header (0 when the count is unavailable);
	 *  - 'filters' the echo-back filter values for the export header;
	 *  - 'range'   array{oldest:int,newest:int} of the oldest and newest event timestamps, or an
	 *              empty array when no rows match or the range query is unavailable.
	 *
	 * The returned stream owns the database connection until it is fully consumed or destroyed:
	 * iterating it to completion and stopping early both release the unbuffered result. The caller
	 * must iterate it (even an empty export) so the connection is unlocked.
	 *
	 * @param array $args Optional arguments to use instead of the HTTP query string.
	 *
	 * @return Revalt<array{stream:\Generator,total:int,filters:array{per_page:int,falist:array<int>,ip:string,login:string,user_id:int,search:string,sid:string,in_url:string,country:string,time_begin:int,time_end:int},range:array{}|array{oldest:int,newest:int}}>
	 *         Success carries the export payload described above.
	 *
	 * @since 9.8.1
	 */
	public static function stream_log( array $args = array() ): Revalt {

		// Export uses the dedicated query variant: full projection, no LIMIT, and companion
		// COUNT and MIN/MAX queries for the header total and date range.

		$parsed = self::parse_query( $args, true );

		// A failed build aborts before any query runs. The parser Revalt is chained for diagnostics.

		if ( $parsed->has_errors() ) {
			return new Revalt( null, 'activity_export_query_build_failed', 'Activity export aborted: unable to build the export query', null, $parsed );
		}

		$built = $parsed->get_results();

		$db_result = warp_get_db();

		if ( $db_result->has_errors() ) {
			return new Revalt( null, 'activity_export_db_unavailable', 'Activity export aborted: database layer unavailable', null, $db_result );
		}

		/** @var CRB_Database $db */
		$db = $db_result->get_results();

		// Resolve the header total first, while the connection is still free. A count failure
		// is non-critical: the header simply reports zero rather than aborting the export.

		$total = 0;
		$count_result = $db->query( $built['count_sql'], CRB_Database::FETCH_OBJECTS );

		if ( ! $count_result->has_errors() ) {
			$count_rows = $count_result->get_results( array() );
			$total = isset( $count_rows[0] ) ? (int) $count_rows[0]->total : 0;
		}

		// Resolve the date range while the connection is still free. Failures and zero-row
		// exports (MIN/MAX return NULL) are non-critical: the header simply omits the range.

		$range = array();

		if ( $built['range_sql'] !== '' ) {
			$range_result = $db->query( $built['range_sql'], CRB_Database::FETCH_OBJECTS );

			if ( ! $range_result->has_errors() ) {
				$range_rows = $range_result->get_results( array() );

				if ( isset( $range_rows[0] ) && $range_rows[0]->oldest !== null && $range_rows[0]->newest !== null ) {
					$range = array(
						'oldest' => (int) $range_rows[0]->oldest,
						'newest' => (int) $range_rows[0]->newest,
					);
				}
			}
		}

		// Open the unbuffered row stream. It owns the connection until fully consumed or closed.
		// A stream failure is critical: abort with a chained error so the caller surfaces it before
		// any CSV output, instead of emitting a misleading empty export.

		$stream_result = $db->query_stream( $built['sql'], CRB_Database::FETCH_OBJECTS );

		if ( $stream_result->has_errors() ) {
			return new Revalt( null, 'activity_export_stream_failed', 'Activity export aborted: unable to open the row stream', null, $stream_result );
		}

		// Wrap the unbuffered result in a generator that releases the connection when the consumer
		// reaches the last row or stops early. Raw row objects are yielded as-is.

		$row_stream = ( static function () use ( $stream_result ): \Generator {

			$row_iterator = $stream_result->get_results();

			try {
				foreach ( $row_iterator as $activity_row ) {
					yield $activity_row;
				}
			} finally {

				// Cleanup unlocks the connection: drop every reference to the unbuffered result.

				$row_iterator = null;
				$stream_result = null;
			}
		} )();

		return new Revalt( array(
			'stream'  => $row_stream,
			'total'   => $total,
			'filters' => $built['filters'],
			'range'   => $range,
		) );
	}

	/**
	 * Parse query string arguments and create an SQL query for retrieving log rows from the activity log table
	 *
	 * @former cerber_activity_query()
	 *
	 * @param array $args Optional arguments to use them instead of using query string
	 * @param bool $export When true, builds the export variant: full projection joined with the
	 *                     users table, no LIMIT, the stream SQL under 'sql', and companion COUNT
	 *                     and MIN/MAX queries compiled into 'count_sql' and 'range_sql' for the
	 *                     streamed total and date range. Set only by stream_log(). When false, the
	 *                     read variant returns the configured query builder under 'builder' for the
	 *                     cached reader to execute and read FOUND_ROWS() from.
	 *
	 * @return Revalt<array{builder:CRB_Query_Builder,filters:array{per_page:int,falist:array<int>,ip:string,login:string,user_id:int,search:string,sid:string,in_url:string,country:string,time_begin:int,time_end:int},calc_on:bool}|array{sql:string,count_sql:string,range_sql:string,filters:array{per_page:int,falist:array<int>,ip:string,login:string,user_id:int,search:string,sid:string,in_url:string,country:string,time_begin:int,time_end:int},calc_on:bool}>
	 *         Success carries the compiled query payload: the read variant carries the configured query builder
	 *         under 'builder', while the export variant carries the compiled SQL bundle under 'sql', 'count_sql'
	 *         and 'range_sql'. On failure the payload is absent and the lower-layer Revalt is chained for
	 *         root-cause diagnostics.
	 *
	 * @since 4.16
	 */
	private static function parse_query( array $args = array(), bool $export = false ): Revalt {
		global $wpdb;

		$filters = self::default_filter_values();

		$q = crb_admin_parse_query( array_keys( self::EVENT_FILTERING_PARAMS ), $args );

		// Pagination, projection mode, and the users table are resolved here so the builder
		// can compile the full statement rather than only the WHERE clause.

		$per_page = ( isset( $args['per_page'] ) ) ? absint( $args['per_page'] ) : crb_admin_get_per_page();
		$page_offset = max( 0, ( crb_get_page_num() - 1 ) * $per_page );
		$calc_on = empty( $args['compact_view'] );
		$users_table = $wpdb->users;

		$filters['per_page'] = $per_page;

		$db_result = warp_get_db();

		if ( $db_result->has_errors() ) {
			return new Revalt( null, 'activity_query_db_unavailable', 'Activity query aborted: database layer unavailable', null, $db_result );
		}

		/** @var CRB_Database $db */
		$db = $db_result->get_results();
		$qb = $db->table( CERBER_LOG_TABLE )->alias( 'log' );

		// Activity filter: an explicit filter set takes precedence over the activity list.
		$falist = array();
		if ( ! empty( $q->filter_set ) ) {
			$falist = crb_get_filter_set( $q->filter_set );
		}
		elseif ( $q->filter_activity ) {
			$falist = crb_sanitize_int( $q->filter_activity );
		}
		if ( $falist ) {
			$qb->where( 'log.activity', 'IN', $falist );
		}
		$filters['falist'] = $falist;

		// Event status filter.
		if ( $q->filter_status ) {
			if ( $status = crb_sanitize_int( $q->filter_status ) ) {
				$qb->where( 'log.ac_status', 'IN', $status );
			}
		}

		// IP filter: a numeric range, a single address, or a guaranteed no-match fallback.
		if ( $q->filter_ip ) {
			$range = cerber_any2range( $q->filter_ip );
			if ( is_array( $range ) ) {
				$qb->where( 'log.ip_long', '>=', $range['begin'] )->where( 'log.ip_long', '<=', $range['end'] );
			}
			elseif ( cerber_is_ip_or_net( $q->filter_ip ) ) {
				$qb->where( 'log.ip', '=', $q->filter_ip );
			}
			else {
				$qb->where( 'log.ip', '=', 'produce-no-result' );
			}
			$filters['ip'] = preg_replace( CRB_IP_NET_RANGE, ' ', $q->filter_ip );
		}

		// Login filter: a pipe-separated list becomes an OR group, otherwise an exact match.
		if ( $q->filter_login ) {
			if ( strpos( $q->filter_login, '|' ) !== false ) {
				$login_conditions = array();
				foreach ( explode( '|', $q->filter_login ) as $single_login ) {
					$login_conditions[] = array( 'log.user_login', '=', $single_login );
				}
				$qb->where_any( $login_conditions );
			}
			else {
				$qb->where( 'log.user_login', '=', $q->filter_login );
			}
			$filters['login'] = crb_escape_html( $q->filter_login );
		}

		// User filter: any logged-in user, or a specific user as the actor or the target.
		if ( isset( $q->filter_user ) ) {
			if ( $q->filter_user == '*' ) {
				$qb->where( 'log.user_id', '!=', 0 );
				$filters['user_id'] = 0;
			}
			elseif ( is_numeric( $q->filter_user ) ) {
				$filter_user_id = absint( $q->filter_user );
				$qb->where_any( array(
					array( 'log.user_id', '=', $filter_user_id ),
					array( 'log.ac_by_user', '=', $filter_user_id ),
				) );
				$filters['user_id'] = $filter_user_id;
			}
		}

		// Free-text search across the login and the IP columns. User-supplied wildcards
		// stay active because the term is not passed through CRB_Database::escape_like().
		if ( $q->search_activity ) {
			$search = stripslashes( $q->search_activity );
			$filters['search'] = crb_escape_html( $search );
			$pattern = '%' . $search . '%';
			$qb->where_any( array(
				array( 'log.user_login', 'LIKE', $pattern ),
				array( 'log.ip', 'LIKE', $pattern ),
			) );
		}

		// Request ID filter (session_id column)
		if ( $q->filter_sid ) {

			// session_id is marked as "Request ID" in the UI

			$sid = (string) $q->filter_sid;

			if ( ! preg_match( '/^[A-Za-z0-9]{1,32}$/', $sid ) ) {
				return new Revalt(
					null,
					'invalid_request_id',
					'Request ID must contain only letters and digits and must not be longer than 32 characters.'
				);
			}

			$qb->where( 'log.session_id', '=', $sid );
			$filters['sid'] = $sid;
		}

		// URL search within the stored request details.
		if ( $q->search_url ) {
			$search = stripslashes( $q->search_url );
			$filters['in_url'] = crb_escape_html( $search );
			$qb->where( 'log.details', 'LIKE', '%' . $search . '%' );
		}

		// Country filter, limited to the stored country code length.
		if ( $q->filter_country ) {
			$country = substr( $q->filter_country, 0, 3 );
			$filters['country'] = crb_escape_html( $country );
			$qb->where( 'log.country', '=', $country );
		}

		// Time range on the event timestamp.
		if ( $begin = $q->filter_time_begin ) {
			$filters['time_begin'] = absint( $begin );
			$qb->where( 'log.stamp', '>=', absint( $begin ) );
		}

		if ( $end = $q->filter_time_end ) {
			$filters['time_end'] = absint( $end );
			$qb->where( 'log.stamp', '<=', absint( $end ) );
		}

		// Projection and pagination depend on the mode, selected explicitly by $export.
		// The export variant joins wp_users for the display name and the aliased login
		// column and omits LIMIT so stream_log() can stream every matching row in a single
		// unbuffered pass.

		$count_sql = '';
		$range_sql = '';

		if ( $export ) {

			// The unbuffered export stream locks the connection while it is consumed, so the
			// total for the CSV header cannot be read with SQL_CALC_FOUND_ROWS/FOUND_ROWS().
			// Compile a separate COUNT query from a snapshot of the filters taken before the
			// projection and ordering are applied.

			$count_qb = clone $qb;
			$count_sql = $count_qb->select_count( '*', 'total' )->to_sql();

			// Companion MIN/MAX query for the header date range, cloned from the same
			// pre-projection snapshot so it carries the identical filters.
			$range_qb = clone $qb;
			$range_sql = $range_qb->select_min( 'log.stamp', 'oldest' )
			                      ->select_max( 'log.stamp', 'newest' )
			                      ->to_sql();

			$qb->select( 'log.*' )
			   ->select( 'u.display_name' )
			   ->select_as( 'u.user_login', 'ulogin' )
			   ->join( $users_table, 'log.user_id', 'u.ID', 'LEFT', 'u' )
			   ->order_by( 'stamp', 'DESC' );
		}
		else {
			$qb->calc_found_rows( $calc_on )
			   ->order_by( 'stamp', 'DESC' );

			if ( $per_page > 0 ) {
				$qb->limit( $per_page )
				   ->offset( $page_offset );
			}
		}

		// Materialize the SQL once so any deferred to_sql() validation (for example a missing LIMIT
		// for a set OFFSET) is recorded in the builder state before it is inspected below.
		$sql = $qb->to_sql();

		// Propagate builder validation failures as a structured error so callers abort instead of
		// running a filters-stripped query. The builder state Revalt is chained for diagnostics.
		if ( $qb->get_state()->has_errors() ) {
			return new Revalt( null, 'activity_query_build_failed', 'Activity query aborted: query builder validation failed', null, $qb->get_state() );
		}

		// The export variant streams raw SQL plus separately compiled COUNT and MIN/MAX companions.
		// The read variant hands the configured builder to the cached reader, which executes it and
		// reads FOUND_ROWS() through the builder's own terminals.
		if ( $export ) {
			return new Revalt( array( 'sql' => $sql, 'count_sql' => $count_sql, 'range_sql' => $range_sql, 'filters' => $filters, 'calc_on' => $calc_on ) );
		}

		return new Revalt( array( 'builder' => $qb, 'filters' => $filters, 'calc_on' => $calc_on ) );
	}

	/**
	 * Returns the echo-back filter values in their default (no-filter) shape.
	 *
	 * Every consumer reads a fixed set of keys from the filters array, so the shape must stay
	 * complete even when query building fails. parse_query() seeds the filters from this, and the
	 * error paths of its callers reuse it to keep the by-reference contract intact.
	 *
	 * @return array{per_page:int,falist:array<int>,ip:string,login:string,user_id:int,search:string,sid:string,in_url:string,country:string,time_begin:int,time_end:int}
	 */
	private static function default_filter_values(): array {
		return array(
			'per_page'   => 0,
			'falist'     => array(),
			'ip'         => '',
			'login'      => '',
			'user_id'    => 0,
			'search'     => '',
			'sid'        => '',
			'in_url'     => '',
			'country'    => '',
			'time_begin' => 0,
			'time_end'   => 0,
		);
	}

	/**
	 * Deletes rows from the activity table using specified values of the given fields.
	 *
	 * @param array $key_fields An associative array of conditional fields for the WHERE clause.
	 *                          Format: ['column_name' => 'value', ...]
	 *
	 * @return int The number of affected rows
	 *
	 * @since 9.6.6.12
	 */
	static function delete( $key_fields = array() ): int {

		if ( ! $key_fields ) {
			return 0;
		}

		if ( $ret = cerber_db_delete_rows( CERBER_LOG_TABLE, $key_fields ) ) {
			self::clear_status();
		}

		return $ret;
	}
}

Home - Capital GMC Buick Regina

No data

Welcome to Capital GMC BUICK – REGINA

Thank you for choosing Capital GMC Buick | Regina, your premier certified Buick and GMC dealership proudly serving drivers in Regina and the surrounding communities. Whether you’re searching for a brand-new Buick or GMC vehicle or a meticulously inspected pre-owned model, we have a diverse selection to match your needs and lifestyle.

Beyond our impressive inventory, we offer a seamless and stress-free financing experience through our well-connected finance centre, where our team of experts is dedicated to securing the best loan or lease options for you, quickly, transparently, and hassle-free.

But our commitment to you doesn’t stop at the sale. Our state-of-the-art service centre is staffed with skilled Buick and GMC technicians who use the latest equipment and genuine OEM parts to keep your vehicle running at its best. From routine maintenance to complex repairs, we’ve got you covered.

Experience top-tier customer service, quality vehicles, and expert care, all in one place. Visit Capital GMC Buick | Regina today or call us at 306-205-8072 with any questions. We’re here to help!

Ask a Question