prefix . 'aieo_conversion_context'; } public static function ensure_schema() { static $ok = null; if ( null !== $ok ) { return $ok; } global $wpdb; $table = self::table(); if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ) { $ok = true; return $ok; } $file = defined( 'AIEO_DIR' ) ? AIEO_DIR . 'includes/dmm/sql/submission-context-schema.php' : ''; if ( '' === $file || ! is_readable( $file ) ) { $ok = false; return $ok; } require_once $file; if ( ! function_exists( 'aieo_install_schema_once' ) && is_readable( AIEO_DIR . 'includes/sql/sql-runtime-helpers.php' ) ) { require_once AIEO_DIR . 'includes/sql/sql-runtime-helpers.php'; } if ( function_exists( 'aieo_install_schema_once' ) ) { delete_option( self::SCHEMA_OPTION ); aieo_install_schema_once( self::SCHEMA_OPTION, self::SCHEMA_VERSION, 'aieo_submission_context_install_schema' ); } else { aieo_submission_context_install_schema(); update_option( self::SCHEMA_OPTION, self::SCHEMA_VERSION, false ); } $ok = ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table ); return $ok; } public static function current_session_id() { foreach ( array( 'aieo_attr_sid', 'aieo_session_id' ) as $c ) { if ( isset( $_COOKIE[ $c ] ) && '' !== $_COOKIE[ $c ] ) { return substr( sanitize_text_field( wp_unslash( (string) $_COOKIE[ $c ] ) ), 0, 64 ); } } return ''; } public static function alt_session_id() { $primary = self::current_session_id(); $alt = isset( $_COOKIE['aieo_session_id'] ) ? substr( sanitize_text_field( wp_unslash( (string) $_COOKIE['aieo_session_id'] ) ), 0, 64 ) : ''; return ( '' !== $alt && $alt !== $primary ) ? $alt : ''; } public static function linked_session_ids( array $ids ) { global $wpdb; $ids = array_values( array_unique( array_filter( array_map( static function ( $id ) { return trim( (string) $id ); }, $ids ) ) ) ); if ( ! $ids ) { return array(); } $t = $wpdb->prefix . 'aieo_session_identity'; if ( (string) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $t ) ) !== $t ) { return $ids; } $in = implode( ',', array_fill( 0, count( $ids ), '%s' ) ); $linked = (array) $wpdb->get_col( $wpdb->prepare( "SELECT session_id FROM `{$t}` WHERE visitor_key IN ({$in}) UNION SELECT visitor_key FROM `{$t}` WHERE session_id IN ({$in}) LIMIT 20", array_merge( $ids, $ids ) ) ); foreach ( $linked as $l ) { $l = trim( (string) $l ); if ( '' !== $l ) { $ids[] = $l; } } return array_values( array_unique( $ids ) ); } public static function attribution_payload( $attr ) { $attr = is_array( $attr ) ? $attr : array(); $out = array(); foreach ( self::FIELDS as $k ) { $out[ $k ] = isset( $attr[ $k ] ) ? substr( (string) $attr[ $k ], 0, 500 ) : ''; } if ( class_exists( 'AIEO_Url_Redaction' ) ) { $out['landing_page'] = AIEO_Url_Redaction::clean( $out['landing_page'] ); $out['referrer'] = AIEO_Url_Redaction::clean( $out['referrer'] ); } if ( class_exists( 'AIEO_Goal_Tracker' ) && method_exists( 'AIEO_Goal_Tracker', 'channel' ) ) { $out['channel'] = (string) AIEO_Goal_Tracker::channel( $attr ); } return $out; } public static function capture( array $args = array() ) { $raw = isset( $args['attribution'] ) && is_array( $args['attribution'] ) ? $args['attribution'] : ( class_exists( 'AIEO_Goal_Tracker' ) ? AIEO_Goal_Tracker::attribution() : array() ); $attribution = self::attribution_payload( $raw ); $sid = self::current_session_id(); $alt = self::alt_session_id(); $ids = self::linked_session_ids( array( $sid, $alt ) ); $page = isset( $args['page_url'] ) && '' !== (string) $args['page_url'] ? (string) $args['page_url'] : (string) wp_get_raw_referer(); $page = class_exists( 'AIEO_Url_Redaction' ) ? AIEO_Url_Redaction::clean( $page ) : $page; return array( 'attribution' => $attribution, 'session_id' => $sid, 'session_alt' => $alt, 'session_ids' => $ids, 'tracked' => $ids ? 1 : 0, 'page_url' => substr( esc_url_raw( $page ), 0, 512 ), 'landing_page' => substr( esc_url_raw( $attribution['landing_page'] ), 0, 512 ), 'referrer' => substr( esc_url_raw( $attribution['referrer'] ), 0, 512 ), 'surface' => self::surface_key( isset( $args['surface'] ) ? $args['surface'] : '' ), ); } public static function surface_key( $surface ) { return substr( (string) preg_replace( '/[^a-z0-9_]/', '', strtolower( (string) $surface ) ), 0, 64 ); } public static function store( $source_key, array $meta, array $context ) { $source_key = substr( (string) $source_key, 0, 64 ); if ( '' === $source_key || ! self::ensure_schema() ) { return false; } global $wpdb; $email = isset( $meta['email'] ) ? strtolower( trim( (string) $meta['email'] ) ) : ''; $row = array( 'source_key' => $source_key, 'provider' => substr( (string) ( $meta['provider'] ?? '' ), 0, 20 ), 'entry_id' => isset( $meta['entry_id'] ) && (int) $meta['entry_id'] > 0 ? (int) $meta['entry_id'] : null, 'goal_key' => substr( (string) ( $meta['goal_key'] ?? '' ), 0, 64 ), 'surface' => self::surface_key( $context['surface'] ?? '' ), 'page_url' => substr( self::redact_url( (string) ( $context['page_url'] ?? '' ) ), 0, 512 ), 'landing_page' => substr( self::redact_url( (string) ( $context['landing_page'] ?? '' ) ), 0, 512 ), 'referrer' => substr( self::redact_url( (string) ( $context['referrer'] ?? '' ) ), 0, 512 ), 'attribution' => wp_json_encode( isset( $context['attribution'] ) && is_array( $context['attribution'] ) ? ( class_exists( 'AIEO_Url_Redaction' ) ? AIEO_Url_Redaction::clean_deep( $context['attribution'] ) : $context['attribution'] ) : array() ), 'session_ids' => wp_json_encode( isset( $context['session_ids'] ) && is_array( $context['session_ids'] ) ? array_values( $context['session_ids'] ) : array() ), 'tracked' => empty( $context['tracked'] ) ? 0 : 1, 'email_hash' => '' !== $email ? hash( 'sha256', $email ) : '', 'occurred_at' => isset( $meta['occurred_at'] ) && '' !== (string) $meta['occurred_at'] ? (string) $meta['occurred_at'] : gmdate( 'Y-m-d H:i:s' ), 'created_at' => gmdate( 'Y-m-d H:i:s' ), ); $table = self::table(); $columns = array_keys( $row ); $holders = array(); $values = array(); foreach ( $row as $k => $v ) { if ( null === $v ) { $holders[] = 'NULL'; continue; } $holders[] = in_array( $k, array( 'entry_id', 'tracked' ), true ) ? '%d' : '%s'; $values[] = $v; } $update = array(); foreach ( $columns as $k ) { if ( in_array( $k, array( 'source_key', 'created_at' ), true ) ) { continue; } $update[] = "`{$k}` = VALUES(`{$k}`)"; } $sql = "INSERT INTO `{$table}` (`" . implode( '`, `', $columns ) . '`) VALUES (' . implode( ', ', $holders ) . ') ON DUPLICATE KEY UPDATE ' . implode( ', ', $update ); $done = $wpdb->query( $wpdb->prepare( $sql, $values ) ); if ( 1 === $done ) { do_action( 'aieo_lead_stored', $source_key, array( 'provider' => $row['provider'], 'entry_id' => (int) $row['entry_id'], 'goal_key' => $row['goal_key'], 'surface' => $row['surface'], 'page_url' => $row['page_url'], 'occurred_at' => $row['occurred_at'], ) ); } return false !== $done; } public static function redact_url( $url ) { return class_exists( 'AIEO_Url_Redaction' ) ? AIEO_Url_Redaction::clean( $url ) : (string) $url; } public static function get( $source_key ) { if ( ! self::ensure_schema() ) { return null; } global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE source_key = %s', self::table(), (string) $source_key ), ARRAY_A ); return $row ? self::decode( $row ) : null; } public static function for_entry( $provider, $entry_id ) { if ( ! self::ensure_schema() ) { return array(); } global $wpdb; $rows = (array) $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE provider = %s AND entry_id = %d ORDER BY occurred_at DESC, id DESC LIMIT 20', self::table(), (string) $provider, (int) $entry_id ), ARRAY_A ); return array_map( array( __CLASS__, 'decode' ), $rows ); } public static function latest_unconfirmed( $provider, $email, $max_age ) { $email = strtolower( trim( (string) $email ) ); if ( '' === $email || ! self::ensure_schema() ) { return ''; } global $wpdb; return (string) $wpdb->get_var( $wpdb->prepare( 'SELECT source_key FROM %i WHERE provider = %s AND email_hash = %s AND confirmed_at IS NULL AND occurred_at >= %s ORDER BY occurred_at DESC, id DESC LIMIT 1', self::table(), (string) $provider, hash( 'sha256', $email ), gmdate( 'Y-m-d H:i:s', time() - max( 0, (int) $max_age ) ) ) ); } public static function confirm( $source_key, $when = '' ) { if ( ! self::ensure_schema() ) { return false; } global $wpdb; $when = '' !== (string) $when ? (string) $when : gmdate( 'Y-m-d H:i:s' ); return false !== $wpdb->query( $wpdb->prepare( 'UPDATE %i SET confirmed_at = %s WHERE source_key = %s AND confirmed_at IS NULL', self::table(), $when, (string) $source_key ) ); } public static function expire_session_ids() { if ( ! self::ensure_schema() ) { return 0; } global $wpdb; $days = max( 1, (int) apply_filters( 'aieo_session_hot_days', (int) get_option( 'aieo_session_hot_days', 3 ) ) ) + max( 1, (int) get_option( 'aieo_session_retention_days', 30 ) ); $n = $wpdb->query( $wpdb->prepare( "UPDATE %i SET session_ids = '[]' WHERE occurred_at < DATE_SUB(UTC_TIMESTAMP(), INTERVAL %d DAY) AND session_ids IS NOT NULL AND session_ids <> '[]'", self::table(), $days ) ); return (int) $n; } public static function register_exporter( $exporters ) { $exporters = is_array( $exporters ) ? $exporters : array(); $exporters['aieo-lead-context'] = array( 'exporter_friendly_name' => __( 'Rooster One form submissions', 'ai-eshop-optimizer' ), 'callback' => array( __CLASS__, 'export' ), ); return $exporters; } public static function register_eraser( $erasers ) { $erasers = is_array( $erasers ) ? $erasers : array(); $erasers['aieo-lead-context'] = array( 'eraser_friendly_name' => __( 'Rooster One form submissions', 'ai-eshop-optimizer' ), 'callback' => array( __CLASS__, 'erase' ), ); return $erasers; } public static function export( $email, $page = 1 ) { $email = strtolower( trim( (string) $email ) ); $per = 100; $page = max( 1, (int) $page ); if ( '' === $email || ! self::ensure_schema() ) { return array( 'data' => array(), 'done' => true ); } global $wpdb; $rows = (array) $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE email_hash = %s ORDER BY id ASC LIMIT %d OFFSET %d', self::table(), hash( 'sha256', $email ), $per, ( $page - 1 ) * $per ), ARRAY_A ); $data = array(); foreach ( $rows as $row ) { $row = self::decode( $row ); $a = $row['attribution']; $form = class_exists( 'AIEO_Submission_Panel' ) ? AIEO_Submission_Panel::surface_label( (string) $row['surface'], (string) $row['provider'] ) : (string) $row['surface']; $items = array( array( 'name' => __( 'Form', 'ai-eshop-optimizer' ), 'value' => $form ), array( 'name' => __( 'Received', 'ai-eshop-optimizer' ), 'value' => (string) $row['occurred_at'] . ' UTC' ), array( 'name' => __( 'Sent from', 'ai-eshop-optimizer' ), 'value' => (string) $row['page_url'] ), array( 'name' => __( 'Landing page', 'ai-eshop-optimizer' ), 'value' => (string) $row['landing_page'] ), array( 'name' => __( 'Referrer', 'ai-eshop-optimizer' ), 'value' => (string) $row['referrer'] ), ); foreach ( array( 'utm_source' => __( 'Source', 'ai-eshop-optimizer' ), 'utm_medium' => __( 'Medium', 'ai-eshop-optimizer' ), 'utm_campaign' => __( 'Campaign', 'ai-eshop-optimizer' ), 'utm_content' => __( 'Content', 'ai-eshop-optimizer' ), 'utm_term' => __( 'Term', 'ai-eshop-optimizer' ), 'gclid' => __( 'Google click id', 'ai-eshop-optimizer' ), 'fbclid' => __( 'Meta click id', 'ai-eshop-optimizer' ), ) as $k => $label ) { if ( isset( $a[ $k ] ) && '' !== (string) $a[ $k ] ) { $items[] = array( 'name' => $label, 'value' => (string) $a[ $k ] ); } } if ( $row['session_ids'] ) { $items[] = array( 'name' => __( 'Visitor ids', 'ai-eshop-optimizer' ), 'value' => implode( ', ', $row['session_ids'] ) ); } if ( ! empty( $row['confirmed_at'] ) ) { $items[] = array( 'name' => __( 'Confirmed', 'ai-eshop-optimizer' ), 'value' => (string) $row['confirmed_at'] . ' UTC' ); } $data[] = array( 'group_id' => 'aieo-lead-context', 'group_label' => __( 'Form submissions (Rooster One)', 'ai-eshop-optimizer' ), 'group_description' => __( 'The campaign each of your form submissions came from, the page it was sent from and the ids your visit was known under.', 'ai-eshop-optimizer' ), 'item_id' => 'aieo-lead-' . (int) $row['id'], 'data' => $items, ); } return array( 'data' => $data, 'done' => count( $rows ) < $per ); } public static function erase( $email, $page = 1 ) { $email = strtolower( trim( (string) $email ) ); if ( '' === $email || ! self::ensure_schema() ) { return array( 'items_removed' => 0, 'items_retained' => false, 'messages' => array(), 'done' => true ); } global $wpdb; $n = (int) $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE email_hash = %s LIMIT 100', self::table(), hash( 'sha256', $email ) ) ); return array( 'items_removed' => $n, 'items_retained' => false, 'messages' => array(), 'done' => $n < 100 ); } public static function decode( array $row ) { $a = json_decode( (string) ( $row['attribution'] ?? '' ), true ); $s = json_decode( (string) ( $row['session_ids'] ?? '' ), true ); $row['attribution'] = is_array( $a ) ? $a : array(); $row['session_ids'] = is_array( $s ) ? array_values( array_map( 'strval', $s ) ) : array(); $row['tracked'] = (int) ( $row['tracked'] ?? 0 ); $row['entry_id'] = isset( $row['entry_id'] ) ? (int) $row['entry_id'] : 0; return $row; } } }