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_Alerts.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!                  |
|                                                                        |
*========================================================================*

*/

/**
 * Dispatches admin alert notifications for a logged activity event.
 *
 * Encapsulates the matching of the subscribed alert rules stored in the
 * CRB_ALL_ALERTS site option against a single activity event and the delivery
 * of a single notification per HTTP request via CRB_Messaging.
 *
 * @since 9.8.1
 */
final class CRB_Activity_Alerts {

	// Positional mapping: parameter names, default values, and position.
	// Critical: the order of these fields MUST NOT be changed because this is a positional array.
	// This array represents the list of fields and their order for each saved alert.
	// This structure is a combination of event parameters and alert settings.
	// See also CRB_Activity::EVENT_FILTERING_PARAMS

	public const CRB_ALERT_PARAMS = array(
		'filter_activity' => 0, // 0
		'filter_user'     => 0, // 1
		'begin'           => 0, // 2 - IPv4 long, synthetic key, value is derivative from filter_ip
		'end'             => 0, // 3 - IPv4 long, synthetic key, value is derivative from filter_ip
		'filter_ip'       => 0, // 4 - IP
		'filter_login'    => 0, // 5
		'search_activity' => 0, // 6
		'filter_role'     => 0, // 7
		'user_ids'        => 0, // 8
		// @since 8.9.6
		'search_url'      => 0, // 9
		'filter_status'   => 0, // 10

		// Non-event alert settings, see CRB_NON_EVENT_PARAMS

		'al_limit'           => 0, // 11 - limit on the number of alerts to send, number
		'al_count'           => 0, // 12 - counts the number of sent alerts, number
		'al_expires'         => 0, // 13 - do not send alerts after this date
		'al_ignore_rate'     => 0, // 14 - ignore global alert limits, flag
		// @since 8.9.7
		'al_send_emails'     => 0, // 15 - send via email, flag
		'al_send_pushbullet' => 0, // 16 - send via Pushbullet, flag
		// @since 9.4.2
		'al_send_me'         => 0, // 17 - send to the email of the user enabled the alert, user id
	);

	// Alert setting fields not related to event parameters

	public const CRB_NON_EVENT_PARAMS = array(
		'al_limit',
		'al_count',
		'al_expires',
		'al_ignore_rate',
		'al_send_emails',
		'al_send_pushbullet',
		'al_send_me',
	);

	/**
	 * Match the current activity event against the subscribed alert rules and
	 * send at most one notification per HTTP request.
	 *
	 * The method reads the alert subscriptions from the CRB_ALL_ALERTS site option,
	 * matches them against the event, sends the first matching alert through
	 * CRB_Messaging, and best-effort persists the updated rate-limit counter.
	 *
	 * @param array{activity:int,status:int,user_id:int,ac_by_user:int,ip:string,ip_long:int,login:string,url:string,country:string} $event Context of the logged activity event.
	 *
	 * @return void
	 */
	public static function dispatch( array $event ): void {

		$alert_list = self::get_alerts_list();

		if ( empty( $alert_list ) ) {
			return;
		}

		$activity = $event['activity'];
		$status = $event['status'];
		$user_id = $event['user_id'];
		$ac_by_user = $event['ac_by_user'];
		$ip = $event['ip'];
		$ip_long = $event['ip_long'];
		$login = $event['login'];
		$url = $event['url'];
		$country = $event['country'];

		$update_alerts = false;

		$alert_config_keys = array_keys( self::CRB_ALERT_PARAMS );

		foreach ( $alert_list as $hash => $alert_config ) {

			$updated = false;

			// Map the positional alert array onto its named fields (CRB_ALERT_PARAMS order).
			// Short legacy alerts are padded and overlong ones truncated to keep the mapping count-safe.

			$alert_values = array_slice( array_pad( array_values( $alert_config ), count( $alert_config_keys ), 0 ), 0, count( $alert_config_keys ) );
			$alert_named = array_combine( $alert_config_keys, $alert_values );

			// Check if all alert parameters match.
			// Not all parameters can exist as they saved in DB when a parameter may not be introduced yet.

			if ( ! empty( $alert_named['filter_status'] )
			     && $alert_named['filter_status'] != $status ) {
				continue;
			}

			if ( ! empty( $alert_named['filter_user'] )
			     && $alert_named['filter_user'] != $user_id
			     && $alert_named['filter_user'] != $ac_by_user ) {
				continue;
			}

			if ( ! empty( $alert_named['al_expires'] )
			     && ( $expires = absint( $alert_named['al_expires'] ) )
			     && $expires < time() ) {
				continue;
			}

			if ( ! empty( $alert_named['al_limit'] ) ) {
				if ( $alert_named['al_limit'] <= $alert_named['al_count'] ) {
					continue;
				}

				$alert_named['al_count'] ++;
				$updated = true;
			}

			if ( ! empty( $alert_named['filter_activity'] ) ) {
				if ( ! in_array( $activity, $alert_named['filter_activity'] ) ) {
					continue;
				}
			}

			if ( ! empty( $alert_named['end'] )
			     && ( $ip_long < $alert_named['begin'] || $alert_named['end'] < $ip_long ) ) {
				continue;
			}

			if ( ! empty( $alert_named['filter_ip'] )
			     && $alert_named['filter_ip'] != $ip ) {
				continue;
			}

			if ( ! empty( $alert_named['filter_login'] )
			     && $alert_named['filter_login'] != $login ) {
				continue;
			}

			if ( ! empty( $alert_named['search_url'] )
			     && false === strpos( $url, $alert_named['search_url'] ) ) {
				continue;
			}

			if ( ! empty( $alert_named['search_activity'] ) ) {
				$search_needle = $alert_named['search_activity'];
				$none = true;
				if ( false !== strpos( $ip, $search_needle ) ) {
					$none = false;
				}
				elseif ( false !== mb_stripos( $login, $search_needle ) ) {
					$none = false;
				}
				elseif ( $user_id && ( $event_user = crb_get_userdata( $user_id ) ) ) {
					if ( false !== mb_stripos( $event_user->user_firstname, $search_needle )
					     || false !== mb_stripos( $event_user->user_lastname, $search_needle )
					     || false !== mb_stripos( $event_user->nickname, $search_needle ) ) {
						$none = false;
					}
				}

				// No alert parameters match, continue to the next alert.

				if ( $none ) {
					continue;
				}
			}

			// Alert parameters match, prepare and send an alert email.

			$ac_lbl = crb_get_activity_label( $activity, $user_id, $ac_by_user, false );

			$status_lbl = '';

			if ( $status ) {
				$status_list = cerber_get_labels( 'status' ) + crb_get_lockout_reason();
				if ( $status_lbl = $status_list[ $status ] ?? '' ) {
					$status_lbl = ' (' . $status_lbl . ')';
				}
			}

			$msg = array();

			/* translators: %s is the activity label with optional status. */
			$msg[] = sprintf( __( 'Activity: %s', 'wp-cerber' ), $ac_lbl . $status_lbl );
			$msg_masked = $msg;

			$coname = $country ? ' (' . crb_get_country_name( $country ) . ')' : '';

			/* translators: %s is the IP address with optional country name. */
			$msg[] = sprintf( __( 'IP address: %s', 'wp-cerber' ), $ip . $coname );
			$msg_masked[] = sprintf( __( 'IP address: %s', 'wp-cerber' ), crb_mask_ip( $ip ) . $coname );

			if ( $user_id ) {
				$u = crb_get_userdata( $user_id );
				/* translators: %s is the user display name. */
				$msg[] = sprintf( __( 'User: %s', 'wp-cerber' ), $u->display_name );
				$msg_masked[] = sprintf( __( 'User: %s', 'wp-cerber' ), $u->display_name );
			}

			if ( $login ) {
				/* translators: %s is the username used for the login attempt. */
				$msg[] = sprintf( __( 'Username used: %s', 'wp-cerber' ), $login );
				$msg_masked[] = sprintf( __( 'Username used: %s', 'wp-cerber' ), crb_mask_login( $login ) );
			}

			if ( ! empty( $alert_named['search_activity'] ) ) {
				/* translators: %s is the search string. */
				$msg[] = sprintf( __( 'Search string: %s', 'wp-cerber' ), $alert_named['search_activity'] );
				$msg_masked[] = sprintf( __( 'Search string: %s', 'wp-cerber' ), $alert_named['search_activity'] );
			}

			// Make links to the Activity log page and to delete this alert.
			// $alert_named is keyed in CRB_ALERT_PARAMS order, so intersecting on the
			// filtering params yields each query parameter aligned with its correct value.

			$activity_params = array_intersect_key( $alert_named, CRB_Activity::EVENT_FILTERING_PARAMS );

			// Note that the links are escaped for safety, and may not be properly rendered in a plain email.

			/* translators: %s is the URL to view activity in the Dashboard. */
			$more = sprintf( __( 'View activity in the Dashboard: %s', 'wp-cerber' ), crb_admin_link_for_html( 'activity', $activity_params ) );
			/* translators: %s is the URL to delete the alert. */
			$more .= "\n\n" . sprintf( __( 'To delete the alert, click here: %s', 'wp-cerber' ), crb_admin_link_for_html( 'activity', [ 'unsubscribeme' => $hash ] ) );

			$ignore = $alert_named['al_ignore_rate'];
			$use_email = ! empty( $alert_named['al_send_emails'] ) || ! empty( $alert_named['al_send_me'] );
			$extra = array();

			if ( ! empty( $alert_named['al_send_me'] ) ) {
				$extra = array( 'user_list' => array( $alert_named['al_send_me'] ) );
			}

			$channels = array( 'email' => $use_email, 'pushbullet' => ! empty( $alert_named['al_send_pushbullet'] ) );

			// Crucial for old alerts (no channels configured at all)

			if ( ! array_filter( $channels ) ) {
				$channels = array(); // All channels are in use
			}

			$sent = CRB_Messaging::send( 'send_alert', array(
				'subj'        => $ac_lbl,
				'text'        => $msg,
				'text_masked' => $msg_masked,
				'more'        => $more,
				'ip'          => $ip
			), $channels, $ignore, $extra );

			if ( $sent && $updated ) {
				$update_alerts = true;
				// Write the incremented counter back in the original positional format.
				$alert_list[ $hash ] = array_values( $alert_named );
			}

			// Only one notification per event makes sense: guards if alert configurations are overlapping.
			// TODO: we should make it channel-aware and recipient-aware: if these settings are different we should allow sending more than one.
			break;
		}

		if ( $update_alerts ) {
			if ( ! update_site_option( CRB_ALL_ALERTS, $alert_list ) ) {
				cerber_error_log( 'Unable to update the list of alerts', 'ALERTS' );
			}
		}


	}

	/**
	 * Returns a list of alert parameters for the currently displaying admin page in a specific order.
	 * The keys are used to create an alert URL.
	 * Values are used to calculate an alert hash.
	 *
	 * @return array<string,mixed> The set of parameters
	 */
	public static function get_alert_params(): array {

		// A set of alert parameters
		// A strictly particular order due to further using numeric array indexes.

		$alert_params = self::CRB_ALERT_PARAMS;
		$query_params = crb_get_query_params();

		if ( ! array_intersect_key( $alert_params, $query_params ) ) {
			return $alert_params; // No parameters in the current request
		}

		// Fill the elements with actual data from the query

		// The IP address field is processing differently than other fields

		if ( ! empty( $query_params['filter_ip'] ) ) {
			$begin = 0;
			$end = 0;

			// Is it a IP range?
			$ip = cerber_any2range( $query_params['filter_ip'] );

			if ( is_array( $ip ) ) {
				$begin = $ip['begin'];
				$end = $ip['end'];
				$ip = 0;
			}
			elseif ( ! $ip ) {
				$ip = 0;
			}

			$alert_params['begin'] = $begin;
			$alert_params['end'] = $end;
			$alert_params['filter_ip'] = $ip;
		}

		// Getting values of the request fields (used as alert parameters) except IP related

		$temp_subset = $alert_params;
		unset( $temp_subset['begin'], $temp_subset['end'], $temp_subset['filter_ip'] );

		foreach ( array_keys( $temp_subset ) as $key ) {
			if ( ! empty( $query_params[ $key ] ) ) {
				if ( is_array( $query_params[ $key ] ) ) {
					$alert_params[ $key ] = array_map( 'trim', $query_params[ $key ] );
				}
				else {
					$alert_params[ $key ] = trim( $query_params[ $key ] );
				}
			}
			else {
				$alert_params[ $key ] = 0;
			}
		}

		// Preparing/sanitizing values of the alert parameters

		if ( ! empty( $alert_params['al_expires'] ) ) {
			$time = 24 * 3600 + strtotime( 'midnight ' . $alert_params['al_expires'] );
			$alert_params['al_expires'] = $time - get_option( 'gmt_offset' ) * 3600;
		}

		$integer_fields = array( 'al_limit', 'al_ignore_rate', 'al_send_emails', 'al_send_pushbullet', 'al_send_me', );

		foreach ( $integer_fields as $int_field ) {
			$alert_params[ $int_field ] = crb_absint( $alert_params[ $int_field ] );
		}

		if ( ! is_array( $alert_params['filter_activity'] ) ) {
			$alert_params['filter_activity'] = array( $alert_params['filter_activity'] );
		}
		$alert_params['filter_activity'] = array_filter( $alert_params['filter_activity'] );

		// Basic XSS sanitization

		array_walk_recursive( $alert_params, function ( &$item ) {
			$item = str_replace( array( '<', '>', '[', ']', '"', "'" ), '', $item );
		} );

		return $alert_params;
	}

	/**
	 * Creates an activity alert from the current admin request parameters.
	 *
	 * Skips creation and shows a notice if an identical alert already exists.
	 *
	 * @return void
	 */
	public static function create_alert() {
		$params = self::get_alert_params();

		if ( self::alert_exists( $params ) ) {
			cerber_admin_notice( 'An alert with given parameters exists.' );

			return;
		}

		$alerts = self::get_alerts_list();
		$alerts[ self::generate_alert_id( $params ) ] = array_values( $params );

		self::save_alerts( $alerts, __( 'The alert has been created', 'wp-cerber' ) );
	}

	/**
	 * Deletes an activity alert by its hash ID.
	 *
	 * @param string $hash Hash ID of the alert. When empty, it is derived from the current request parameters.
	 *
	 * @return void
	 */
	public static function delete_alert( string $hash = '' ) {
		if ( ! $hash ) {
			$hash = self::generate_alert_id( self::get_alert_params() );
		}

		$alerts = self::get_alerts_list();

		if ( ! isset( $alerts[ $hash ] ) ) {
			crb_add_admin_error( 'No alert with the given ID found' );

			return;
		}

		unset( $alerts[ $hash ] );

		self::save_alerts( $alerts, __( 'The alert has been deleted', 'wp-cerber' ) );
	}

	/**
	 * Returns the stored list of activity alerts.
	 *
	 * @return array<string,array> Map of alert hash to its stored values.
	 */
	static function get_alerts_list(): array {
		$alerts = cerber_get_site_option( CRB_ALL_ALERTS );

		return ( $alerts && is_array( $alerts ) ) ? $alerts : array();
	}

	/**
	 * Persists the list of activity alerts and reports the result to the admin.
	 *
	 * @param array<string,array> $alerts Full list of alerts to store.
	 * @param string $success_msg Message shown on a successful update.
	 *
	 * @return bool
	 */
	private static function save_alerts( array $alerts, string $success_msg = '' ): bool {
		if ( update_site_option( CRB_ALL_ALERTS, $alerts ) ) {
			cerber_admin_message( $success_msg );

			return true;
		}
		else {
			crb_add_admin_error( 'Unable to update alerts!' );

			return false;
		}
	}

	/**
	 * Convert old alert format: adding new parameters to existing alerts and updating their hashes.
	 *
	 * @return bool
	 *
	 * @since 8.9.5.5
	 */
	public static function migrate(): bool {
		if ( ! $alerts = self::get_alerts_list() ) {
			return true;
		}

		$test = current( $alerts );

		if ( isset( $test[9] ) ) {
			return true;
		}

		// Old structure needs to be converted

		$new_format = array();

		foreach ( $alerts as $alert ) {
			$alert[9] = 0;
			$alert[10] = 0;
			$new_format[ self::generate_alert_id( $alert ) ] = $alert;
		}

		if ( ! self::save_alerts( $new_format ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Delete expired alerts.
	 *
	 * @return bool True on success or if no alerts exist, false on save failure.
	 */
	public static function delete_expired(): bool {
		if ( ! $alerts = self::get_alerts_list() ) {
			return true;
		}

		$has_changes = false;

		foreach ( $alerts as $hash => $alert ) {
			if ( ! self::is_alert_expired( $alert ) ) {
				continue;
			}

			unset( $alerts[ $hash ] );
			$has_changes = true;
		}

		if ( ! $has_changes ) {
			return true;
		}

		if ( ! self::save_alerts( $alerts ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Check if the given alert has expired.
	 *
	 * @param array $alert The alert data array.
	 *
	 * @return bool True if expired, false otherwise.
	 *
	 * @since 8.9.5.4
	 */
	public static function is_alert_expired( array $alert ): bool {
		// Check if the threshold count has reached or exceeded the maximum allowed limit.
		if ( ! empty( $alert[11] ) && ! empty( $alert[12] ) && $alert[11] <= $alert[12] ) {
			return true;
		}

		// Check if the explicit expiration timestamp has passed.
		if ( ! empty( $alert[13] ) && $alert[13] < time() ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if an alert with the given parameters exists.
	 *
	 * @param array $params Optional alert parameters. When empty, derived from the current request.
	 *
	 * @return bool
	 *
	 * @since 8.9.5.6
	 */
	public static function alert_exists( array $params = array() ): bool {

		$alerts = self::get_alerts_list();

		if ( ! $alerts ) {
			return false;
		}

		if ( ! $params ) {
			$params = self::get_alert_params();
		}

		$hash = self::generate_alert_id( $params );

		if ( isset( $alerts[ $hash ] )
		     && ! self::is_alert_expired( $alerts[ $hash ] ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Generates an alert ID based on its filtering parameters
	 *
	 * @param array $alert_params
	 *
	 * @return string 40-character hexadecimal number used as ID
	 *
	 * @since 8.9.6
	 */
	public static function generate_alert_id( array $alert_params ): string {
		return sha1( json_encode( array_values( array_diff_key( $alert_params, array_flip( CRB_Activity_Alerts::CRB_NON_EVENT_PARAMS ) ) ) ) );
	}

}