File "class-itsec-fingerprint.php"
Full Path: /home/siazco/grocery.siazco.se/wp-content/plugins/better-wp-security/core/lib/fingerprinting/class-itsec-fingerprint.php
File size: 20.25 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Class ITSEC_Fingerprint
*/
class ITSEC_Fingerprint implements JsonSerializable {
const S_APPROVED = 'approved';
const S_AUTO_APPROVED = 'auto-approved';
const S_PENDING_AUTO_APPROVE = 'pending-auto-approve';
const S_PENDING = 'pending';
const S_IGNORED = 'ignored';
const S_DENIED = 'denied';
/** @var WP_User */
private $user;
/** @var DateTime */
private $created_at;
/** @var ITSEC_Fingerprint_Value[] */
private $values = array();
/** @var int */
private $_id;
/** @var int */
private $_uses = 0;
/** @var string */
private $_status = self::S_PENDING;
/** @var string */
private $_uuid;
/** @var DateTime */
private $_last_seen;
/** @var DateTime */
private $_approved_at;
/** @var string */
private $_hash;
/** @var array */
private $_snapshot = array();
/**
* ITSEC_Fingerprint constructor.
*
* @param WP_User $user
* @param DateTime $time
* @param ITSEC_Fingerprint_Value[] $values
*/
public function __construct( WP_User $user, DateTime $time, array $values ) {
$this->user = $user;
$this->created_at = $this->_last_seen = $time;
foreach ( $values as $value ) {
$this->values[ $value->get_source()->get_slug() ] = $value;
}
}
/**
* Compare this fingerprint with another fingerprint.
*
* The operation is not commutative, if a source is missing in the given fingerprint that is present in the current fingerprint,
* it will count as a 0 score, whereas when the given fingerprint has extra source values, those will not impact the score.
*
* @param ITSEC_Fingerprint $fingerprint
*
* @return ITSEC_Fingerprint_Comparison
*/
public function compare( ITSEC_Fingerprint $fingerprint ) {
$scores = array();
$total_weight = 0;
foreach ( $this->values as $value ) {
$source = $value->get_source();
$other = $fingerprint->values[ $source->get_slug() ];
$weight = $source->get_weight( $value );
if ( $other ) {
$scores[ $source->get_slug() ] = array(
'score' => $source->compare( $value, $other ),
'weight' => $weight,
);
} else {
$scores[ $source->get_slug() ] = array(
'score' => 0,
'weight' => $weight,
);
}
$total_weight += $weight;
}
$final_score = 0;
foreach ( $scores as $score ) {
$percent = $score['weight'] / $total_weight;
$final_score += $score['score'] * $percent;
}
return new ITSEC_Fingerprint_Comparison( $this, $fingerprint, $final_score, $scores );
}
/**
* Is the fingerprint approved.
*
* @return bool
*/
public function is_approved() { return self::S_APPROVED === $this->_status; }
/**
* Is the fingerprint auto-approved.
*
* @return bool
*/
public function is_auto_approved() { return self::S_AUTO_APPROVED === $this->_status; }
/**
* Is the fingerprint pending auto-approval.
*
* @return bool
*/
public function is_pending_auto_approval() { return self::S_PENDING_AUTO_APPROVE === $this->_status; }
/**
* Is the fingerprint in pending status.
*
* @return bool
*/
public function is_pending() { return self::S_PENDING === $this->_status; }
/**
* Is the fingerprint being ignored.
*
* @return bool
*/
public function is_ignored() { return self::S_IGNORED === $this->_status; }
/**
* Is the fingerprint denied.
*
* @return bool
*/
public function is_denied() { return self::S_DENIED === $this->_status; }
/**
* Can the fingerprint's status be changed.
*
* @return bool
*/
public function can_change_status() {
return $this->is_auto_approved() ||
$this->is_pending_auto_approval() ||
$this->is_pending() ||
$this->is_ignored();
}
/**
* Get the number of times the fingerprint was used.
*
* @return int
*/
public function get_uses() {
return $this->_uses;
}
/**
* Get the WordPress user this fingerprint is for.
*
* @return WP_User
*/
public function get_user() {
return $this->user;
}
/**
* Get the time the fingerprint was created.
*
* @return DateTime
*/
public function get_created_at() {
return $this->created_at;
}
/**
* Get the values making up this fingerprint.
*
* @return ITSEC_Fingerprint_Value[]
*/
public function get_values() {
return $this->values;
}
/**
* Get the UUID associated with this fingerprint.
*
* @return string
*/
public function get_uuid() {
return $this->_uuid;
}
/**
* Get the status of the fingerprint.
*
* @return string
*/
public function get_status() {
return $this->_status;
}
/**
* Get the time the fingerprint was approved at.
*
* @return DateTime|null
*/
public function get_approved_at() {
return $this->_approved_at;
}
/**
* Get the date the fingerprint was last seen.
*
* @return DateTime
*/
public function get_last_seen() {
return $this->_last_seen;
}
/**
* Get a snapshot of user or system configuration values at the time this fingerprint was created.
*
* @return array
*/
public function get_snapshot() {
return $this->_snapshot;
}
/**
* Get a hash uniquely identifying the collected data.
*
* @return string
*/
public function calculate_hash() {
if ( $this->_hash ) {
return $this->_hash;
}
if ( ! $serialized = $this->serialize_values() ) {
return null;
}
return md5( $serialized );
}
/**
* Set the last seen time for the Fingerprint.
*
* @return bool
*/
public function was_seen() {
$this->_uses ++;
$this->_last_seen = new DateTime( '@' . ITSEC_Core::get_current_time_gmt(), new DateTimeZone( 'UTC' ) );
if ( ! $this->_id ) {
return true;
}
return $this->save( 'was_seen' );
}
/**
* Approve this fingerprint.
*
* @return bool
*/
public function approve() {
if ( self::S_APPROVED === $this->_status ) {
return true;
}
if ( ! $this->can_change_status() ) {
return false;
}
$this->_status = self::S_APPROVED;
$this->_approved_at = new DateTime( '@' . ITSEC_Core::get_current_time_gmt(), new DateTimeZone( 'UTC' ) );
return $this->_id ? $this->save( $this->get_status_action( self::S_APPROVED ) ) : true;
}
/**
* Approve this fingerprint.
*
* @return bool
*/
public function auto_approve() {
if ( self::S_AUTO_APPROVED === $this->_status ) {
return true;
}
if ( ! $this->can_change_status() ) {
return false;
}
$this->_status = self::S_AUTO_APPROVED;
$this->_approved_at = new DateTime( '@' . ITSEC_Core::get_current_time_gmt(), new DateTimeZone( 'UTC' ) );
return $this->_id ? $this->save( $this->get_status_action( self::S_AUTO_APPROVED ) ) : true;
}
/**
* Delay auto-approval for a few days.
*
* @return bool
*/
public function delay_auto_approve() {
if ( self::S_PENDING_AUTO_APPROVE === $this->_status ) {
return true;
}
if ( ! $this->is_pending() ) {
return false;
}
$this->_status = self::S_PENDING_AUTO_APPROVE;
return $this->_id ? $this->save( $this->get_status_action( self::S_PENDING_AUTO_APPROVE ) ) : true;
}
/**
* Ignore a device.
*
* This basically treats a device as "Pending",
* but removes is from the list of Login Alerts.
*
* @return bool
*/
public function ignore() {
if ( self::S_IGNORED === $this->_status ) {
return true;
}
if ( ! $this->can_change_status() ) {
return false;
}
$this->_status = self::S_IGNORED;
return $this->_id ? $this->save( $this->get_status_action( self::S_IGNORED ) ) : true;
}
/**
* Deny this fingerprint.
*
* @return bool
*/
public function deny() {
if ( self::S_DENIED === $this->_status ) {
return true;
}
if ( ! $this->can_change_status() ) {
return false;
}
$this->_status = self::S_DENIED;
return $this->_id ? $this->save( $this->get_status_action( self::S_DENIED ) ) : true;
}
/**
* Set the fingerprint's status.
*
* This should almost never be used. Instead use the status-specific methods above.
*
* @internal
*
* @param string $status
*
* @return bool
*/
public function _set_status( $status ) {
if ( $status === $this->_status ) {
return true;
}
if ( ! $this->_id ) {
return false;
}
$this->_status = $status;
return $this->save( $this->get_status_action( $status ), 'override' );
}
/**
* Get the action suffix to use when changing a status.
*
* @param string $status
*
* @return string
*/
private function get_status_action( $status ) {
switch ( $status ) {
case self::S_APPROVED:
return 'approved';
case self::S_AUTO_APPROVED:
return 'auto_approved';
case self::S_PENDING_AUTO_APPROVE:
return 'auto_approve_delayed';
case self::S_IGNORED:
return 'ignored';
case self::S_DENIED:
return 'denied';
default:
return $status;
}
}
/**
* Create the fingerprint in storage.
*
* @return bool
*/
public function create() {
if ( $this->_id ) {
return false;
}
if ( ! $data = $this->serialize_values() ) {
return false;
}
if ( ! $this->get_uses() ) {
$this->_uses = 1;
}
global $wpdb;
$this->_uuid = wp_generate_uuid4();
$this->generate_snapshot();
$rows_affected = $wpdb->insert( $wpdb->base_prefix . 'itsec_fingerprints', array(
'fingerprint_user' => $this->get_user()->ID,
'fingerprint_hash' => md5( $data ),
'fingerprint_data' => $data,
'fingerprint_uses' => $this->_uses,
'fingerprint_status' => $this->_status,
'fingerprint_uuid' => $this->_uuid,
'fingerprint_created_at' => $this->get_created_at()->format( 'Y-m-d H:i:s' ),
'fingerprint_last_seen' => $this->get_last_seen()->format( 'Y-m-d H:i:s' ),
'fingerprint_approved_at' => $this->get_approved_at() ? $this->get_approved_at()->format( 'Y-m-d H:i:s' ) : '',
'fingerprint_snapshot' => wp_json_encode( $this->_snapshot ),
), array(
'fingerprint_user' => '%d',
'fingerprint_hash' => '%s',
'fingerprint_data' => '%s',
'fingerprint_uses' => '%d',
'fingerprint_status' => '%s',
'fingerprint_uuid' => '%s',
'fingerprint_created_at' => '%s',
'fingerprint_last_seen' => '%s',
'fingerprint_approved_at' => '%s',
'fingerprint_snapshot' => '%s',
) );
if ( $rows_affected ) {
$this->_id = $wpdb->insert_id;
/**
* Fires when a fingerprint is created.
*
* @param ITSEC_Fingerprint $this
*/
do_action( 'itsec_fingerprint_created', $this );
if ( self::S_PENDING !== $this->_status ) {
$action = $this->get_status_action( $this->_status );
do_action( "itsec_fingerprint_{$action}", $this, $action );
}
}
return (bool) $rows_affected;
}
/**
* Serialize the values for storage.
*
* @return false|string
*/
private function serialize_values() {
$data = array();
foreach ( $this->get_values() as $value ) {
$data[ $value->get_source()->get_slug() ] = $value->get_value();
}
return wp_json_encode( $data );
}
/**
* Generate the snapshot of user/system configuration.
*/
private function generate_snapshot() {
if ( ! $this->_snapshot ) {
$this->_snapshot = array(
'user_email' => $this->get_user()->user_email,
);
}
}
/**
* Save the current state.
*
* @param string $action
* @param mixed $additional,...
*
* @return bool
*/
private function save( $action = '', $additional = null ) {
global $wpdb;
$updated = (bool) $wpdb->update(
$wpdb->base_prefix . 'itsec_fingerprints',
array(
'fingerprint_last_seen' => $this->get_last_seen()->format( 'Y-m-d H:i:s' ),
'fingerprint_uses' => $this->get_uses(),
'fingerprint_status' => $this->get_status(),
'fingerprint_approved_at' => $this->get_approved_at() ? $this->get_approved_at()->format( 'Y-m-d H:i:s' ) : '',
),
array( 'fingerprint_id' => $this->_id ),
array( 'fingerprint_last_seen' => '%s', 'fingerprint_uses' => '%d', 'fingerprint_status' => '%s', 'fingerprint_approved_at' => '%s' ),
array( 'fingerprint_id' => '%d' )
);
if ( $updated && $action ) {
$args = array_merge( array( $this, $action ), array_slice( func_get_args(), 1 ) );
/**
* Fires when the fingerprint is saved.
*
* @param ITSEC_Fingerprint $this
* @param string $action
*/
do_action_ref_array( "itsec_fingerprint_{$action}", $args );
}
return $updated;
}
private static function build_where_clause( string $sql, WP_User $user, array $args ) {
global $wpdb;
$sql .= "WHERE `fingerprint_user` = %s";
$prepare = array( $user->ID );
if ( ! empty( $args['status'] ) ) {
if ( is_array( $args['status'] ) ) {
$sql .= ' AND `fingerprint_status` IN (' . implode( ', ', array_fill( 0, count( $args['status'] ), '%s' ) ) . ')';
$prepare = array_merge( $prepare, $args['status'] );
} else {
$sql .= ' AND `fingerprint_status` = %s';
$prepare[] = $args['status'];
}
}
if ( ! empty( $args['exclude'] ) ) {
$sql .= ' AND `fingerprint_uuid` NOT IN (' . implode( ', ', array_fill( 0, count( $args['exclude'] ), '%s' ) ) . ')';
$prepare = array_merge( $prepare, wp_parse_slug_list( $args['exclude'] ) );
}
if ( ! empty( $args['last_seen_before'] ) ) {
$before = date( 'Y-m-d H:i:s', $args['last_seen_before'] );
$sql .= ' AND `fingerprint_last_seen` <= %s';
$prepare[] = $before;
}
if ( ! empty( $args['last_seen_after'] ) ) {
$after = date( 'Y-m-d H:i:s', $args['last_seen_after'] );
$sql .= ' AND `fingerprint_last_seen` >= %s';
$prepare[] = $after;
}
if ( ! empty( $args['search'] ) ) {
$sql .= ' AND `fingerprint_data` LIKE %s';
$prepare[] = '%"%' . $wpdb->esc_like( $args['search'] ) . '%"%';
}
return [
'sql' => $sql,
'prepare' => $prepare,
];
}
/**
* Get a user's fingerprints.
*
* @param WP_User $user
* @param array $args
*
* @return ITSEC_Fingerprint[]
*/
public static function get_all_for_user( WP_User $user, array $args ) {
global $wpdb;
$sql = "SELECT * FROM {$wpdb->base_prefix}itsec_fingerprints ";
$build_results = self::build_where_clause( $sql, $user, $args );
$build_results['sql'] .= ' ORDER BY `fingerprint_last_seen` DESC';
if (
! empty( $args['page'] ) &&
! empty( $args['per_page'] ) &&
is_int( $args['per_page'] ) &&
is_int( $args['page'] )
) {
$limit = $args['per_page'];
if ( $limit > 0 ) {
$offset = ( $args['page'] - 1 ) * $limit;
$build_results['sql'] .= " LIMIT $offset,$limit";
}
}
$rows = $wpdb->get_results( $wpdb->prepare( $build_results['sql'], $build_results['prepare'] ) );
$fingerprints = array();
foreach ( $rows as $row ) {
if ( $fingerprint = self::_hydrate_fingerprint( $row ) ) {
$fingerprints[] = $fingerprint;
}
}
return $fingerprints;
}
/**
* @param WP_User $user
* @param array $args
*
* @return integer
*/
public static function count_all_for_user( WP_User $user, array $args ) {
global $wpdb;
$sql = "SELECT COUNT(*) FROM {$wpdb->base_prefix}itsec_fingerprints ";
$args['count'] = true;
$build_results = self::build_where_clause( $sql, $user, $args );
return $wpdb->get_var( $wpdb->prepare( $build_results['sql'], $build_results['prepare'] ) );
}
/**
* Get a fingerprint by its UUID.
*
* @param string $uuid
*
* @return ITSEC_Fingerprint|null
*/
public static function get_by_uuid( $uuid ) {
global $wpdb;
$row = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->base_prefix}itsec_fingerprints WHERE `fingerprint_uuid` = %s", $uuid ) );
if ( ! $row ) {
return null;
}
return self::_hydrate_fingerprint( $row );
}
/**
* Get a fingerprint by its data hash.
*
* @param WP_User $user
* @param string $hash
*
* @return ITSEC_Fingerprint|null
*/
public static function get_by_hash( WP_User $user, $hash ) {
global $wpdb;
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT * FROM {$wpdb->base_prefix}itsec_fingerprints WHERE `fingerprint_hash` = %s AND `fingerprint_user` = %s",
$hash,
$user->ID
) );
if ( ! $row ) {
return null;
}
return self::_hydrate_fingerprint( $row );
}
/**
* Hydrate a fingerprint with data from the database.
*
* @internal
*
* @param object $row
*
* @return ITSEC_Fingerprint|null
*/
public static function _hydrate_fingerprint( $row ) {
$sources = ITSEC_Lib_Fingerprinting::get_sources();
$values = array();
foreach ( json_decode( $row->fingerprint_data, true ) as $slug => $value ) {
if ( isset( $sources[ $slug ] ) ) {
$values[] = new ITSEC_Fingerprint_Value( $sources[ $slug ], $value );
}
}
if ( ! $user = get_userdata( $row->fingerprint_user ) ) {
return null;
}
$fingerprint = new ITSEC_Fingerprint(
$user,
new DateTime( $row->fingerprint_created_at, new DateTimeZone( 'UTC' ) ),
$values
);
$approved_at = $row->fingerprint_approved_at && $row->fingerprint_approved_at !== '0000-00-00 00:00:00' ? $row->fingerprint_approved_at : null;
$fingerprint->_id = $row->fingerprint_id;
$fingerprint->_uses = $row->fingerprint_uses;
$fingerprint->_status = $row->fingerprint_status;
$fingerprint->_uuid = $row->fingerprint_uuid;
$fingerprint->_hash = $row->fingerprint_hash;
$fingerprint->_last_seen = new DateTime( $row->fingerprint_last_seen, new DateTimeZone( 'UTC' ) );
$fingerprint->_approved_at = $approved_at ? new DateTime( $approved_at, new DateTimeZone( 'UTC' ) ) : null;
if ( $row->fingerprint_snapshot ) {
$fingerprint->_snapshot = json_decode( $row->fingerprint_snapshot, true );
}
return $fingerprint;
}
/**
* Get a summary of this fingerprint.
*
* @return string
*/
public function __toString() {
$location = $browser = $platform = $ip = '';
if ( isset( $this->values['ip'] ) ) {
$ip = $this->values['ip']->get_value();
require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-geolocation.php' );
if ( ! is_wp_error( $geolocate = ITSEC_Lib_Geolocation::geolocate( $ip ) ) ) {
$location = $geolocate['label'];
}
}
if ( isset( $this->values['header-user-agent'] ) ) {
require_once( ITSEC_Core::get_core_dir() . 'lib/class-itsec-lib-browser.php' );
$browser_lib = new ITSEC_Lib_Browser( $this->values['header-user-agent']->get_value() );
$browser = "{$browser_lib->getBrowser() } ({$browser_lib->getVersion()})";
$platform = $browser_lib->getPlatform();
}
if ( $location && $browser ) {
$str = sprintf( esc_html__( 'Device running %1$s on %2$s near %3$s', 'better-wp-security' ), $browser, $platform, $location );
} elseif ( $location ) {
$str = sprintf( esc_html__( 'Device near %1$s', 'better-wp-security' ), $location );
} elseif ( $browser ) {
$str = sprintf( esc_html__( 'Device running %1$s on %2$s', 'better-wp-security' ), $browser, $platform );
} else {
$str = '';
}
if ( $ip ) {
$str .= " ($ip)";
}
return trim( $str );
}
/**
* Serialize a fingerprint to JSON.
*
* For uses when you need to persist a fingerprint that hasn't been stored yet
* to short term storage.
*
* @return array
*/
public function jsonSerialize(): ?array {
if ( $this->_id ) {
return null;
}
$approved_at = $this->get_approved_at();
$values = array();
foreach ( $this->get_values() as $value ) {
$values[ $value->get_source()->get_slug() ] = $value->get_value();
}
return [
'user' => $this->get_user()->ID,
'created_at' => $this->get_created_at()->format( 'Y-m-d H:i:s' ),
'values' => $values,
'status' => $this->get_status(),
'uses' => $this->get_uses(),
'approved_at' => $approved_at ? $approved_at->format( 'Y-m-d H:i:s' ) : null,
];
}
/**
* Recreate a fingerprint from JSON.
*
* @param string $json
*
* @return ITSEC_Fingerprint|null
*/
public static function from_json( $json ) {
$decoded = json_decode( $json, true );
if ( ! $decoded ) {
return null;
}
ITSEC_Lib::load( 'fingerprinting' );
$sources = ITSEC_Lib_Fingerprinting::get_sources();
$values = array();
foreach ( $decoded['values'] as $slug => $value ) {
if ( isset( $sources[ $slug ] ) ) {
$values[] = new ITSEC_Fingerprint_Value( $sources[ $slug ], $value );
}
}
$fingerprint = new ITSEC_Fingerprint(
get_userdata( $decoded['user'] ),
new DateTime( $decoded['created_at'], new DateTimeZone( 'UTC' ) ),
$values
);
$fingerprint->_status = $decoded['status'];
$fingerprint->_uses = $decoded['uses'];
$fingerprint->_approved_at = $decoded['approved_at'] ? new DateTime( $decoded['approved_at'], new DateTimeZone( 'UTC' ) ) : null;
return $fingerprint;
}
}