url( [ 'account' => $this->number( 'account' ), 'order' => $this->number( 'order' ), 'paged' => max( 1, $this->number( 'paged' ) ) ] ) );
exit;
}
public function styles( $hook ) {
if ( 'toplevel_page_activity-track-ai' === $hook && ATKAI_Admin_Menus::is_woocommerce_tab() ) {
wp_enqueue_style( 'atk-security', plugins_url( 'assets/admin/css/atk.security.css', ATKAI_PLUGIN_FILE ), [], filemtime( ATKAI_ABSPATH . 'assets/admin/css/atk.security.css' ) );
wp_enqueue_script( 'atk-security-overview', plugins_url( 'assets/admin/js/atk.security.js', ATKAI_PLUGIN_FILE ), [], filemtime( ATKAI_ABSPATH . 'assets/admin/js/atk.security.js' ), false );
wp_localize_script( 'atk-security-overview', 'atkSecurityOverview', [
'key' => 'atk-woocommerce-overview-' . get_current_blog_id() . '-' . get_current_user_id(),
'show' => __( 'Show overview', 'activity-track' ),
'hide' => __( 'Hide overview', 'activity-track' ),
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'atk_woocommerce_history' ),
'loading' => __( 'Loading recorded history...', 'activity-track' ),
'error' => __( 'History could not be loaded. Please try again.', 'activity-track' ),
'history' => __( 'Activity History', 'activity-track' ),
'details' => __( 'Event Details', 'activity-track' ),
'retry' => __( 'Retry', 'activity-track' ),
] );
}
}
private function number( $name ) {
return isset( $_GET[ $name ] ) && is_scalar( $_GET[ $name ] ) ? absint( $_GET[ $name ] ) : 0;
}
private function url( $args = [] ) {
return add_query_arg( array_merge( [ 'page' => 'activity-track-ai', 'tab' => 'woocommerce' ], $args ), admin_url( 'admin.php' ) );
}
private function filters() {
$text = static function( $key ) { return isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ? sanitize_key( wp_unslash( $_GET[ $key ] ) ) : ''; };
$event = $text( 'event' );
$result = $text( 'result' );
$days = $this->number( 'days' );
return [ 'account' => $this->number( 'account' ), 'order' => $this->number( 'order' ), 'days' => in_array( $days, [ 1, 7, 30 ], true ) ? $days : 0,
'event' => in_array( $event, self::event_types(), true ) ? $event : '', 'result' => array_key_exists( $result, self::outcomes() ) ? $result : '',
'group_orders' => defined( 'ATKAI_PRO_PLUGIN_ACTIVE' ) && ATKAI_PRO_PLUGIN_ACTIVE && 1 === $this->number( 'group_orders' ) ? 1 : 0 ];
}
private static function event_types() {
return [ 'account_created', 'profile_updated', 'checkout_submitted', 'order_paid', 'payment_error', 'payment_method_added', 'stripe_payment', 'card_setup' ];
}
private function commerce_where() {
return "(event_type IN ('checkout_submitted','order_paid','payment_error','payment_method_added','stripe_payment','card_setup') OR (event_type IN ('account_created','profile_updated') AND source IN ('Checkout','My account')))";
}
private static function outcomes() {
return [ 'recorded' => __( 'Recorded', 'activity-track' ), 'error' => __( 'Processing error', 'activity-track' ), 'failed' => __( 'Failed', 'activity-track' ), 'succeeded' => __( 'Succeeded', 'activity-track' ), 'requires_action' => __( 'Authentication needed', 'activity-track' ), 'processing' => __( 'Processing', 'activity-track' ) ];
}
private function where( $filters ) {
global $wpdb;
$where = $this->commerce_where();
if ( $filters['account'] ) $where .= $wpdb->prepare( ' AND subject_id = %d', $filters['account'] );
if ( $filters['order'] ) $where .= $wpdb->prepare( ' AND order_id = %d', $filters['order'] );
if ( $filters['days'] ) $where .= $wpdb->prepare( ' AND occurred_at >= %s', gmdate( 'Y-m-d H:i:s', time() - $filters['days'] * DAY_IN_SECONDS ) );
if ( $filters['event'] ) $where .= $wpdb->prepare( ' AND event_type = %s', $filters['event'] );
if ( $filters['result'] ) $where .= $wpdb->prepare( ' AND outcome = %s', $filters['result'] );
return $where;
}
public static function label( $type ) {
$labels = [
'account_created' => __( 'Account created', 'activity-track' ),
'login' => __( 'Account signed in', 'activity-track' ),
'profile_updated' => __( 'Account profile updated', 'activity-track' ),
'role_changed' => __( 'Account role changed', 'activity-track' ),
'checkout_submitted' => __( 'Checkout submitted', 'activity-track' ),
'order_paid' => __( 'WooCommerce recorded payment complete', 'activity-track' ),
'payment_error' => __( 'Stripe checkout reported an error', 'activity-track' ),
'payment_method_added' => __( 'Saved payment method added', 'activity-track' ),
'stripe_payment' => __( 'Stripe payment outcome', 'activity-track' ),
'card_setup' => __( 'Stripe card setup outcome', 'activity-track' ),
];
return $labels[ $type ] ?? __( 'Account activity', 'activity-track' );
}
public static function profile_description( $reason ) {
$labels = [ 'contact' => __( 'Contact details', 'activity-track' ), 'name' => __( 'Name', 'activity-track' ), 'password' => __( 'Password', 'activity-track' ), 'billing_address' => __( 'Billing address', 'activity-track' ), 'shipping_address' => __( 'Shipping address', 'activity-track' ) ];
$changed = array_intersect_key( $labels, array_flip( explode( ',', $reason ) ) );
return $changed ? sprintf( /* translators: %s: comma-separated categories of changed profile fields. */ __( 'Changed: %s. Values are not stored.', 'activity-track' ), implode( ', ', $changed ) ) : __( 'Changed fields were not captured for this event.', 'activity-track' );
}
/** Group only adjacent profile records for the same subject, actor, source and mode within one minute. */
public static function event_groups( $rows ) {
$groups = [];
foreach ( $rows as $row ) {
$index = count( $groups ) - 1;
$first = $index >= 0 ? $groups[ $index ][0] : null;
if ( $first && empty( $row->order_event_count ) && empty( $first->order_event_count ) && 'profile_updated' === $row->event_type && $first->event_type === $row->event_type && $row->subject_id && $first->subject_id === $row->subject_id && $first->actor_id === $row->actor_id && $first->source === $row->source && $first->mode === $row->mode && $first->order_id === $row->order_id && abs( strtotime( $first->occurred_at ) - strtotime( $row->occurred_at ) ) <= 60 ) {
$groups[ $index ][] = $row;
} else {
$groups[] = [ $row ];
}
}
return $groups;
}
private function render_user( $id ) {
global $wp_roles;
$user = $id ? get_user_by( 'id', $id ) : false;
if ( $user && $user->ID ) {
$role = isset( $user->roles[0], $wp_roles->role_names[ $user->roles[0] ] ) ? $wp_roles->role_names[ $user->roles[0] ] : __( 'Unknown', 'activity-track' );
echo '' . get_avatar( $user->ID, 30 ) . '' . esc_html( $user->display_name ) . '' . esc_html( sprintf( /* translators: %d: WordPress user ID. */ __( 'User ID %d', 'activity-track' ), $user->ID ) ) . '' . esc_html( $role ) . '';
} elseif ( $id ) {
echo esc_html( sprintf( /* translators: %d: unavailable WordPress user ID. */ __( 'User ID %d', 'activity-track' ), $id ) ) . '' . esc_html__( 'Account unavailable', 'activity-track' ) . '';
} else echo esc_html__( 'Guest or unlinked', 'activity-track' );
}
private function evidence( $group ) {
foreach ( $group as $event ) {
echo ' ' . esc_html__( 'The gateway reported an error. A bank decline or fraud is not confirmed by this event.', 'activity-track' ) . '' . esc_html( self::label( $event->event_type ) ) . '
' . esc_html( '#' . $event->event_id . ' · ' . $event->source ) . '
';
if ( 'payment_error' === $event->event_type ) echo '' . esc_html( $event->gateway_ref ) . '' . esc_html( $event->reason ) . '
| ' . esc_html( $heading ) . ' | '; echo '||||
|---|---|---|---|---|
| ' . esc_html__( 'No recorded events match this view.', 'activity-track' ) . ' | ||||
| ' . esc_html( sprintf( /* translators: %s: human-readable time difference. */ __( '%s ago', 'activity-track' ), human_time_diff( strtotime( $row->occurred_at . ' UTC' ), time() ) ) ) . ' ' . esc_html( get_date_from_gmt( $row->occurred_at, 'M j, Y H:i:s' ) ) . ' | '; $this->render_user( $row->subject_id ); echo ' | '; $order = $row->order_id && function_exists( 'wc_get_order' ) ? wc_get_order( $row->order_id ) : null; if ( $order && is_callable( [ $order, 'get_edit_order_url' ] ) ) echo '' . esc_html( '#' . $row->order_id ) . ''; elseif ( $row->order_id ) echo esc_html( '#' . $row->order_id ); else echo '-'; echo ' | ' . esc_html( $result ) . ''; if ( ! empty( $row->order_event_count ) ) echo '' . esc_html__( 'Latest matching result', 'activity-track' ) . ''; if ( 'test' === $row->mode ) echo '' . esc_html__( 'Test mode', 'activity-track' ) . ''; if ( $row->reason && 'profile_updated' !== $row->event_type ) echo '' . esc_html( ucfirst( str_replace( '_', ' ', $row->reason ) ) ) . ''; echo ' | ' . esc_html( self::label( $row->event_type ) ) . '';
if ( ! empty( $row->order_event_count ) ) echo '' . esc_html( sprintf( /* translators: %d: number of grouped order events. */ _n( '%d event', '%d events', $row->order_event_count, 'activity-track' ), $row->order_event_count ) ) . '';
elseif ( count( $group ) > 1 ) echo '' . esc_html( sprintf( /* translators: %d: number of grouped profile records. */ _n( '%d record', '%d records', count( $group ), 'activity-track' ), count( $group ) ) ) . '';
echo ' ' . esc_html( $row->source ) . '';
if ( $history ) {
echo '' . esc_html__( 'Evidence details', 'activity-track' ) . ''; $this->evidence( $group ); echo '';
if ( $row->order_id || $row->subject_id ) echo '';
echo ' ';
$this->evidence( $group );
echo '';
}
echo ' |
' . esc_html( sprintf( /* translators: %s: number of retained history records. */ _n( '%s retained record', '%s retained records', $total, 'activity-track' ), number_format_i18n( $total ) ) . ' · ' . wp_timezone_string() ) . ( 'test' === $anchor->mode ? ' · ' . esc_html__( 'Test mode', 'activity-track' ) : '' ) . '
'; if ( 'order' === $scope ) { $paid = $wpdb->get_var( $wpdb->prepare( "SELECT MAX(occurred_at) FROM %i WHERE $where AND event_type = 'order_paid'", $table ) ); echo '' . esc_html( $paid ? __( 'Payment completion recorded by WooCommerce in retained order history.', 'activity-track' ) : __( 'No WooCommerce payment-completion record in retained order history. This does not confirm that the order is unpaid.', 'activity-track' ) ) . '
'; } $this->event_table( $rows, true ); echo '' . esc_html( $description ) . '
' . esc_html__( 'Open Activity Track settings', 'activity-track' ) . ' — ' . esc_html__( 'Under Enable Activity Types, select User and WooCommerce as needed, then save settings.', 'activity-track' ) . '
'; } echo '' . esc_html__( 'Monitoring only — no customers are blocked. Only activity captured since tracking was enabled appears here.', 'activity-track' ) . '
' . esc_html( sprintf( /* translators: 1: WordPress user ID, 2: number of live Stripe failure events. */ __( 'User ID %1$d has %2$d Stripe failure events in the last 10 minutes. This may indicate card testing or legitimate retries.', 'activity-track' ), $signal->subject_id, $signal->failures ) ) . '
' . esc_html__( 'Review customer activity', 'activity-track' ) . '