self::MAX_BODY_BYTES ) { return 'too_large'; } $minute = gmdate( 'YmdHi' ); $all = 'flareops_bcn_all_' . $minute; $n = (int) get_transient( $all ); if ( $n >= self::MAX_REQUESTS_PER_MINUTE ) { return 'rate_limited'; } set_transient( $all, $n + 1, 2 * MINUTE_IN_SECONDS ); $ip = Util::server_ip( 'REMOTE_ADDR' ); $key = 'flareops_bcn_c_' . substr( hash_hmac( 'sha256', $ip . '|' . $minute, wp_salt( 'nonce' ) ), 0, 20 ); $c = (int) get_transient( $key ); if ( $c >= self::CLIENT_PER_MINUTE ) { return 'rate_limited'; } set_transient( $key, $c + 1, 2 * MINUTE_IN_SECONDS ); return null; } /** * Handle a beacon. Stores aggregate counters only: no IP, no IDs, no cookies. * * @return array{ok:bool,counted:bool,reason?:string} */ public static function record( string $referrer, string $utm, string $path ): array { if ( ! Settings::feature( 'referral_beacon' ) ) { return array( 'ok' => true, 'counted' => false, 'reason' => 'disabled' ); } $referrer = mb_substr( Util::strip_url_query( $referrer ), 0, 2048 ); // never keep query strings / fragments $utm = mb_substr( sanitize_text_field( $utm ), 0, 100 ); $source = Util::ai_source( $referrer, $utm ); if ( null === $source ) { return array( 'ok' => true, 'counted' => false, 'reason' => 'not_ai' ); } $bucket = 'flareops_beacon_rl_' . gmdate( 'YmdHi' ); $n = (int) get_transient( $bucket ); if ( $n >= self::RATE_PER_MINUTE ) { return array( 'ok' => true, 'counted' => false, 'reason' => 'rate_limited' ); } set_transient( $bucket, $n + 1, 2 * MINUTE_IN_SECONDS ); $path = (string) wp_parse_url( '/' . ltrim( $path, '/' ), PHP_URL_PATH ); $path = mb_substr( '' === $path ? '/' : $path, 0, 255 ); $path = preg_replace( '/[^\x20-\x7E\x{80}-\x{10FFFF}]/u', '', $path ); $pid = url_to_postid( home_url( $path ) ); $pid = ( $pid && 'product' === get_post_type( $pid ) ) ? (int) $pid : null; global $wpdb; $t = Install::table( 'referrals' ); $day = gmdate( 'Y-m-d' ); // phpcs:disable WordPress.DB $id = $wpdb->get_var( $wpdb->prepare( 'SELECT id FROM %i WHERE day = %s AND source = %s AND landing_path = %s LIMIT 1', $t, $day, $source, $path ) ); if ( $id ) { $wpdb->query( $wpdb->prepare( 'UPDATE %i SET visits = visits + 1 WHERE id = %d', $t, (int) $id ) ); } else { $wpdb->insert( $t, array( 'day' => $day, 'source' => $source, 'landing_path' => $path, 'product_id' => $pid, 'visits' => 1, ) ); } // phpcs:enable return array( 'ok' => true, 'counted' => true, 'source' => $source ); } public static function report( string $since ): array { global $wpdb; // phpcs:disable WordPress.DB $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT day, source, landing_path, product_id, visits FROM %i WHERE day >= %s ORDER BY day ASC, visits DESC', Install::table( 'referrals' ), $since ), ARRAY_A ); // phpcs:enable $daily = array_map( static fn( $r ) => array( 'date' => substr( (string) $r['day'], 0, 10 ), 'source' => $r['source'], 'landing_path' => $r['landing_path'], 'product_id' => $r['product_id'] ? (int) $r['product_id'] : null, 'visits' => (int) $r['visits'], ), (array) $rows ); $orders = array(); if ( Util::woo_active() && function_exists( 'wc_get_orders' ) ) { $list = Util::safe( static fn() => wc_get_orders( array( 'limit' => 500, 'orderby' => 'date', 'order' => 'DESC', 'date_created' => '>=' . strtotime( $since . ' 00:00:00 UTC' ), 'type' => 'shop_order', ) ), array() ); foreach ( (array) $list as $o ) { if ( ! is_object( $o ) || ! method_exists( $o, 'get_meta' ) ) { continue; } $type = (string) $o->get_meta( '_wc_order_attribution_source_type' ); $utm = (string) $o->get_meta( '_wc_order_attribution_utm_source' ); $ref = (string) $o->get_meta( '_wc_order_attribution_referrer' ); $dc = $o->get_date_created(); $orders[] = array( // Salted SHA-256 (per-site salt), not the raw order ID. 'order_id' => Util::order_ref( (int) $o->get_id() ), 'date' => $dc ? $dc->date( 'c' ) : null, 'total' => (string) $o->get_total(), 'currency' => (string) $o->get_currency(), 'status' => (string) $o->get_status(), 'source_type' => $type, 'utm_source' => $utm, 'referrer' => Util::strip_url_query( $ref ), 'ai_source' => Util::ai_source( $ref, $utm ), ); } } return array( 'daily' => $daily, 'orders' => $orders, ); } }