get_meta( '_agent_stamp_verdict' ) ) { return; } $via = method_exists( $order, 'get_created_via' ) ? (string) $order->get_created_via() : ''; if ( ! in_array( $via, array( 'checkout', 'store-api', 'admin' ), true ) && ! self::is_admin_order_screen() && ! self::is_api_channel( $via ) ) { return; } self::apply_stamp( $order, true ); } /** * Orders an API client or a commerce protocol created in this request. * Renewals, imports run by cron, and other background orders are left alone. * * @param string $via * @return bool */ private static function is_api_channel( $via ) { if ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) { return false; } return AgentStamp_Classifier::is_api_channel( $via ); } /** * @param WC_Order $order * @param bool $save */ private static function apply_stamp( $order, $save ) { static $stamping = false; if ( $stamping || ! is_object( $order ) || ! method_exists( $order, 'update_meta_data' ) ) { return; } $stamping = true; if ( '' !== (string) $order->get_meta( '_agent_stamp_verdict' ) ) { $stamping = false; return; } $placed_in_admin = self::is_admin_order_screen() || ( method_exists( $order, 'get_created_via' ) && 'admin' === (string) $order->get_created_via() ); if ( $placed_in_admin ) { $result = array( 'verdict' => 'human', 'name' => '', 'confidence' => 'browser', 'detail' => 'Placed from the WordPress admin.', ); } else { // Shared with the rules and the funnel: a signature nonce can only be claimed once per request. $request = AgentStamp_Sensor::request(); $result = AgentStamp_Sensor::classification(); // WooCommerce verified its checkout nonce before this hook. These fields only label the order. $client = AgentStamp_Witness::read_client( isset( $_POST ) && is_array( $_POST ) ? $_POST : array(), // phpcs:ignore WordPress.Security.NonceVerification.Missing isset( $_COOKIE ) && is_array( $_COOKIE ) ? $_COOKIE : array(), isset( $request->headers['cf-bot-score'] ) ? $request->headers['cf-bot-score'] : null ); $result = AgentStamp_Witness::merge( $result, $client ); $via = method_exists( $order, 'get_created_via' ) ? (string) $order->get_created_via() : ''; if ( self::is_api_channel( $via ) ) { $result = AgentStamp_Classifier::for_channel( $result, $request->user_agent, $via ); } $evidence = AgentStamp_Evidence::encode( AgentStamp_Evidence::build( $result, $request, $via, time(), AGENT_STAMP_VERSION ) ); $order->update_meta_data( '_agent_stamp_evidence', $evidence ); $order->update_meta_data( '_agent_stamp_fingerprint', AgentStamp_Evidence::fingerprint( $evidence ) ); $order->update_meta_data( '_agent_stamp_presence', $result['presence'] ); $ua = $request->user_agent; if ( strlen( $ua ) > 1024 ) { $ua = substr( $ua, 0, 1024 ); } $order->update_meta_data( '_agent_stamp_ua', $ua ); AgentStamp_Policy::mark_order( $order, $result, $via ); AgentStamp_Sensor::stamped( $order, $result, $via ); } $order->update_meta_data( '_agent_stamp_verdict', $result['verdict'] ); $order->update_meta_data( '_agent_stamp_name', $result['name'] ); $order->update_meta_data( '_agent_stamp_confidence', $result['confidence'] ); $order->update_meta_data( '_agent_stamp_detail', $result['detail'] ); if ( $save && method_exists( $order, 'save_meta_data' ) && $order->get_id() ) { $order->save_meta_data(); self::note_order( $order ); } $stamping = false; } /** * One private order note when the buyer was not a person, so the stamp sits in the order history. * * @param int|WC_Order $order_or_id */ public static function note_order( $order_or_id ) { $order = is_object( $order_or_id ) ? $order_or_id : ( function_exists( 'wc_get_order' ) ? wc_get_order( $order_or_id ) : null ); if ( ! $order || ! method_exists( $order, 'add_order_note' ) || ! $order->get_id() ) { return; } $verdict = (string) $order->get_meta( '_agent_stamp_verdict' ); if ( '' === $verdict || 'human' === $verdict || '' !== (string) $order->get_meta( '_agent_stamp_noted' ) ) { return; } $note = sprintf( /* translators: 1: stamp label, 2: reason */ __( 'Agent Stamp: %1$s. %2$s', 'agent-stamp' ), self::order_label( $order ), (string) $order->get_meta( '_agent_stamp_detail' ) ); $fingerprint = (string) $order->get_meta( '_agent_stamp_fingerprint' ); if ( '' !== $fingerprint ) { $note .= ' ' . sprintf( /* translators: %s: sha256 fingerprint */ __( 'Evidence fingerprint %s.', 'agent-stamp' ), $fingerprint ); } $order->update_meta_data( '_agent_stamp_noted', '1' ); $order->save_meta_data(); $order->add_order_note( $note ); do_action( 'agent_stamp_not_human_order', $order ); } /** * @return bool */ private static function is_admin_order_screen() { if ( ! is_admin() ) { return false; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return false; } return current_user_can( 'edit_shop_orders' ); } /** * @param array $columns * @return array */ public static function add_column( $columns ) { $updated = array(); $added = false; foreach ( $columns as $key => $label ) { $updated[ $key ] = $label; if ( 'order_status' === $key ) { $updated['agent_stamp'] = __( 'Buyer', 'agent-stamp' ); $added = true; } } if ( ! $added ) { $updated['agent_stamp'] = __( 'Buyer', 'agent-stamp' ); } return $updated; } /** * @param string $column * @param WC_Order $order */ public static function render_hpos_column( $column, $order ) { if ( 'agent_stamp' !== $column ) { return; } echo esc_html( self::order_label( $order ) ); } /** * @param string $column * @param int $post_id */ public static function render_legacy_column( $column, $post_id ) { if ( 'agent_stamp' !== $column || ! function_exists( 'wc_get_order' ) ) { return; } echo esc_html( self::order_label( wc_get_order( $post_id ) ) ); } public static function add_metabox() { $screen = 'shop_order'; if ( function_exists( 'wc_get_page_screen_id' ) ) { $screen = wc_get_page_screen_id( 'shop-order' ); } add_meta_box( 'agent-stamp', __( 'Who placed this order', 'agent-stamp' ), array( __CLASS__, 'render_metabox' ), $screen, 'side', 'high' ); } /** * @param WP_Post|WC_Order $post_or_order */ public static function render_metabox( $post_or_order ) { $order = ( is_object( $post_or_order ) && method_exists( $post_or_order, 'get_meta' ) ) ? $post_or_order : ( isset( $post_or_order->ID ) ? wc_get_order( $post_or_order->ID ) : null ); if ( ! $order ) { echo esc_html__( 'No order.', 'agent-stamp' ); return; } echo '

' . esc_html( self::order_label( $order ) ) . '

'; $detail = (string) $order->get_meta( '_agent_stamp_detail' ); if ( '' !== $detail ) { echo '

' . esc_html( $detail ) . '

'; } $ua = (string) $order->get_meta( '_agent_stamp_ua' ); if ( '' !== $ua ) { echo '

' . esc_html( $ua ) . '

'; } $fingerprint = (string) $order->get_meta( '_agent_stamp_fingerprint' ); if ( '' !== $fingerprint ) { echo '

' . esc_html( sprintf( /* translators: %s: start of the sha256 fingerprint */ __( 'Evidence recorded, fingerprint %s.', 'agent-stamp' ), substr( $fingerprint, 0, 19 ) ) ) . '

'; } if ( '' !== (string) $order->get_meta( '_agent_stamp_evidence' ) && current_user_can( 'edit_shop_orders' ) ) { $url = wp_nonce_url( admin_url( 'admin-post.php?action=agent_stamp_evidence&order=' . $order->get_id() ), 'agent_stamp_evidence_' . $order->get_id() ); echo '

' . esc_html__( 'Download evidence (JSON)', 'agent-stamp' ) . '

'; } foreach ( AgentStamp_Payment::evidence_lines( $order ) as $line ) { echo '

' . esc_html( $line ) . '

'; } $hold = (string) $order->get_meta( '_agent_stamp_hold' ); if ( '' !== $hold ) { echo '

' . esc_html( AgentStamp_Policy::hold_note( $hold ) ) . '

'; } do_action( 'agent_stamp_order_box_after', $order ); } /** * The order's evidence record as a JSON file, for a dispute or a chargeback reply. */ public static function download_evidence() { $order_id = isset( $_GET['order'] ) ? absint( $_GET['order'] ) : 0; if ( ! $order_id || ! current_user_can( 'edit_shop_orders' ) ) { wp_die( esc_html__( 'You cannot download this evidence.', 'agent-stamp' ), 403 ); } check_admin_referer( 'agent_stamp_evidence_' . $order_id ); $order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : null; if ( ! $order ) { wp_die( esc_html__( 'No order.', 'agent-stamp' ), 404 ); } $created = $order->get_date_created(); $file = AgentStamp_Evidence::file( array( 'number' => (string) $order->get_order_number(), 'created' => $created ? $created->format( 'c' ) : '', 'total' => (string) $order->get_total(), 'currency' => (string) $order->get_currency(), 'payment_method' => (string) $order->get_payment_method_title(), 'transaction_id' => (string) $order->get_transaction_id(), 'customer_ip' => (string) $order->get_customer_ip_address(), 'store' => home_url( '/' ), ), (string) $order->get_meta( '_agent_stamp_evidence' ), (string) $order->get_meta( '_agent_stamp_fingerprint' ) ); nocache_headers(); header( 'Content-Disposition: attachment; filename="agent-stamp-order-' . sanitize_file_name( (string) $order->get_order_number() ) . '.json"' ); wp_send_json( $file, null, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } public static function enqueue_checkout() { if ( ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) { return; } wp_enqueue_script( 'agent-stamp-checkout', plugins_url( 'assets/checkout.js', AGENT_STAMP_FILE ), array(), AGENT_STAMP_VERSION, true ); } /** * The settings page stylesheet. Freemius can move the menu, so match the page, not the hook suffix. */ public static function enqueue_admin() { // Read-only check of which admin page is open. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'agent-stamp' !== $page ) { return; } wp_enqueue_style( 'agent-stamp-admin', plugins_url( 'assets/admin.css', AGENT_STAMP_FILE ), array(), AGENT_STAMP_VERSION ); } public static function checkout_fields() { static $printed = false; if ( $printed || ! function_exists( 'is_checkout' ) || ! is_checkout() || is_order_received_page() ) { return; } $printed = true; echo ''; wp_print_inline_script_tag( (string) wp_json_encode( array( 'identify' => 'Put your product name in the agent name field, or send Agent/Name in the user-agent. A Web Bot Auth signature is proof. An agent that names itself or signs is not refused.' ) ), array( 'type' => 'application/json', 'id' => 'agent-stamp-invite', ) ); } /** * @param WC_Order|false $order * @return string */ public static function order_label( $order ) { if ( ! $order || ! method_exists( $order, 'get_meta' ) ) { return __( 'Not checked', 'agent-stamp' ); } $result = array( 'verdict' => (string) $order->get_meta( '_agent_stamp_verdict' ), 'name' => (string) $order->get_meta( '_agent_stamp_name' ), 'confidence' => (string) $order->get_meta( '_agent_stamp_confidence' ), 'detail' => (string) $order->get_meta( '_agent_stamp_detail' ), ); if ( '' === $result['verdict'] ) { return __( 'Not checked', 'agent-stamp' ); } return AgentStamp_Classifier::label( $result ); } public static function send_digest() { if ( 'no' === get_option( 'agent_stamp_send_digest', 'yes' ) ) { return; } if ( ! function_exists( 'wc_get_orders' ) ) { return; } $rows = self::stamped_rows( time() - 7 * DAY_IN_SECONDS ); $lines = array(); $signed = 0; $declared = 0; $unattended = 0; $unknown = 0; $api = 0; $likely = 0; $paid_by_agent = 0; foreach ( $rows as $row ) { $verdict = $row['verdict']; if ( ! in_array( $verdict, array( 'agent', 'likely', 'unknown', 'unattended', 'api' ), true ) ) { continue; } if ( 'likely' === $verdict ) { $likely++; } elseif ( 'api' === $verdict ) { $api++; } elseif ( 'unknown' === $verdict ) { $unknown++; } elseif ( 'unattended' === $verdict ) { $unattended++; } elseif ( 'signed' === $row['confidence'] ) { $signed++; } elseif ( 'payment' === $row['confidence'] ) { $paid_by_agent++; } else { $declared++; } $order = $row['order']; $lines[] = sprintf( '#%s %s %s', $order->get_order_number(), self::order_label( $order ), html_entity_decode( wp_strip_all_tags( $order->get_formatted_order_total() ), ENT_QUOTES, 'UTF-8' ) ); } $funnel = AgentStamp_Funnel::summarize_sessions( AgentStamp_Events::sessions( time() - 7 * DAY_IN_SECONDS ) ); if ( 0 === $signed + $declared + $paid_by_agent + $likely + $unattended + $api + $unknown && 0 === $funnel['sessions'] ) { return; } $body = "Agent Stamp, last 7 days\n\n"; $summary = AgentStamp_Report::summarize( $rows ); if ( $summary['orders'] > 0 ) { $not_human = AgentStamp_Report::not_human( $summary ); $body .= sprintf( "%d of %d orders were not a person. Revenue from them: %s.\n\n", $not_human['orders'], $summary['orders'], html_entity_decode( wp_strip_all_tags( wc_price( $not_human['revenue'] ) ), ENT_QUOTES, 'UTF-8' ) ); } $body .= self::funnel_text( $funnel ); $body .= 'Signed agents: ' . $signed . "\n"; $body .= 'Declared agents: ' . $declared . "\n"; $body .= 'Agents named by the payment: ' . $paid_by_agent . "\n"; $body .= 'Likely agents (not proof): ' . $likely . "\n"; $body .= 'Unattended: ' . $unattended . "\n"; $body .= 'API or protocol: ' . $api . "\n"; $body .= 'Unknown: ' . $unknown . "\n\n"; $body .= implode( "\n", $lines ); $body .= "\n\nOpen WooCommerce > Agent Stamp for each agent and the fix for each failure.\n"; $body = (string) apply_filters( 'agent_stamp_digest_body', $body, $rows ); wp_mail( get_option( 'admin_email' ), sprintf( /* translators: %s: site name */ __( 'Agent Stamp for %s', 'agent-stamp' ), wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) ), $body ); } /** * The funnel as a few lines of plain text for the weekly email. * * @param array $funnel AgentStamp_Funnel::summarize() result. * @return string */ private static function funnel_text( array $funnel ) { $scripts = $funnel['scripts'] > 0 ? sprintf( "Scripts reached the cart %d %s, %d with an error. Card testers look like this.\n", $funnel['scripts'], 1 === $funnel['scripts'] ? 'time' : 'times', $funnel['script_failures'] ) : ''; if ( 0 === $funnel['sessions'] ) { return "No AI agent visits were recorded.\n" . $scripts . "\n"; } $money = static function ( $amount ) { return function_exists( 'wc_price' ) ? html_entity_decode( wp_strip_all_tags( wc_price( $amount ) ), ENT_QUOTES, 'UTF-8' ) : number_format( (float) $amount, 2 ); }; $text = sprintf( "AI agents visited %d %s. %d reached a cart, %d reached checkout, and they paid for %d %s (%s).\n", $funnel['sessions'], 1 === $funnel['sessions'] ? 'time' : 'times', $funnel['cart'], $funnel['checkout'], $funnel['orders'], 1 === $funnel['orders'] ? 'order' : 'orders', $money( $funnel['revenue'] ) ); if ( $funnel['failed'] > 0 ) { $text .= sprintf( "%d %s hit an error from the store. Carts stopped that way: %s.\n", $funnel['failed'], 1 === $funnel['failed'] ? 'visit' : 'visits', $money( $funnel['lost_value'] ) ); } $shown = 0; foreach ( $funnel['reasons'] as $reason ) { if ( 'no_error' === $reason['reason'] ) { continue; } if ( ++$shown > 3 ) { break; } $explain = AgentStamp_Funnel::explain( $reason['reason'] ); $text .= sprintf( "- %s: %d. %s\n", $explain['title'], $reason['sessions'], $explain['fix'] ); } return $text . $scripts . "\n"; } public static function add_settings_page() { add_submenu_page( 'woocommerce', __( 'Agent Stamp', 'agent-stamp' ), __( 'Agent Stamp', 'agent-stamp' ), 'manage_woocommerce', 'agent-stamp', array( __CLASS__, 'render_settings' ) ); } /** * Stamped orders created since a time, as rows for AgentStamp_Report. * * @param int $since * @return array */ public static function stamped_rows( $since ) { $rows = array(); $page = 1; do { $orders = wc_get_orders( array( 'limit' => 200, 'paged' => $page, 'date_created' => '>=' . $since, 'return' => 'objects', 'status' => array_keys( wc_get_order_statuses() ), 'type' => 'shop_order', ) ); foreach ( $orders as $order ) { if ( in_array( $order->get_status(), array( 'failed', 'cancelled', 'checkout-draft' ), true ) ) { continue; } $rows[] = array( 'verdict' => (string) $order->get_meta( '_agent_stamp_verdict' ), 'name' => (string) $order->get_meta( '_agent_stamp_name' ), 'confidence' => (string) $order->get_meta( '_agent_stamp_confidence' ), 'total' => (float) $order->get_total(), 'order' => $order, ); } $page++; } while ( count( $orders ) === 200 && $page <= 25 ); return $rows; } /** * The orders list URL for this store's order storage. * * @return string */ public static function orders_list_url() { if ( class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { return admin_url( 'admin.php?page=wc-orders' ); } return admin_url( 'edit.php?post_type=shop_order' ); } /** * @return int */ public static function report_days() { // Read-only view choice, checked against a fixed list. $raw = isset( $_GET['period'] ) ? sanitize_key( wp_unslash( $_GET['period'] ) ) : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return AgentStamp_Report::pick( $raw ); } private static function render_report() { $days = self::report_days(); $rows = self::stamped_rows( time() - $days * DAY_IN_SECONDS ); $summary = AgentStamp_Report::summarize( $rows ); echo '

' . esc_html( sprintf( /* translators: %d: number of days */ _n( 'Last %d day', 'Last %d days', $days, 'agent-stamp' ), $days ) ) . '

'; echo '

'; foreach ( AgentStamp_Report::periods() as $index => $period ) { $label = sprintf( /* translators: %d: days */ __( 'Last %d days', 'agent-stamp' ), $period ); if ( $index > 0 ) { echo ' · '; } if ( $period === $days ) { echo '' . esc_html( $label ) . ''; } else { echo '' . esc_html( $label ) . ''; } } echo '

'; if ( 0 === $summary['orders'] ) { echo '

' . esc_html__( 'No stamped orders yet. New orders are stamped as they are placed.', 'agent-stamp' ) . '

'; } else { $not_human = AgentStamp_Report::not_human( $summary ); echo '

' . esc_html( sprintf( /* translators: 1: order count, 2: total order count, 3: revenue */ __( '%1$d of %2$d orders were not a person. Revenue from them: %3$s.', 'agent-stamp' ), $not_human['orders'], $summary['orders'], wp_strip_all_tags( wc_price( $not_human['revenue'] ) ) ) ) . '

'; self::render_breakdown( $summary ); } do_action( 'agent_stamp_report_after', $summary, $rows, $days ); if ( ! defined( 'AGENT_STAMP_PRO_VERSION' ) ) { echo '

' . esc_html__( 'Agent Stamp Pro is a separate plugin. It adds the change against the period before, a client report, the Buyer filter on the orders list, CSV export, alerts, and email support, 24/7.', 'agent-stamp' ) . ' '; AgentStamp_Admin::pro_link(); echo '

'; } } /** * Orders and revenue for each buyer label. * * @param array{lines:array} $summary AgentStamp_Report::summarize() result. */ private static function render_breakdown( array $summary ) { echo '

' . esc_html__( 'By buyer', 'agent-stamp' ) . '

'; echo ''; foreach ( $summary['lines'] as $line ) { echo ''; } echo '
' . esc_html__( 'Buyer', 'agent-stamp' ) . '' . esc_html__( 'Orders', 'agent-stamp' ) . '' . esc_html__( 'Revenue', 'agent-stamp' ) . '' . esc_html__( 'Share', 'agent-stamp' ) . '
' . esc_html( $line['label'] ) . '' . esc_html( (string) $line['orders'] ) . '' . esc_html( wp_strip_all_tags( wc_price( $line['revenue'] ) ) ) . '' . esc_html( round( $line['share'] * 100 ) . '%' ) . '
'; } public static function render_settings() { if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } echo '
'; echo '

' . esc_html__( 'Agent Stamp', 'agent-stamp' ) . '

'; $tab = AgentStamp_Admin::render_tabs(); if ( 'orders' === $tab ) { echo '

' . esc_html__( 'Each new order is stamped Human, a named agent, Likely agent, Unattended, API, or Unknown.', 'agent-stamp' ) . '

'; self::render_report(); AgentStamp_Scan::render(); } elseif ( 'readiness' === $tab ) { AgentStamp_Admin::render_readiness(); } elseif ( 'rules' === $tab ) { AgentStamp_Admin::render_rules(); do_action( 'agent_stamp_settings_after' ); } else { AgentStamp_Admin::render_agents(); } echo '
'; } }