prefix . 'dealroot_live'; } /** * Whether the tracker reports open pages. * * @return bool */ public static function enabled() { return (bool) Settings::get( 'live_view' ); } /** * Notes a sign of an open page. * * @param string $key Random key of the browser tab. * @param array $sign Page, title, form, state ("", "filling" or "sent"), and the nudge the * visitor saw with nudge_state ("shown" or "clicked"). * @return array{ok: bool, push: string, fast: bool} Push: a message to show the visitor, * sent from the live screen; fast: someone watches the live screen. */ public static function record( $key, array $sign ) { global $wpdb; $answer = array( 'ok' => false, 'push' => '', 'fast' => self::watched(), ); if ( ! preg_match( '/^[a-f0-9]{32}$/', (string) $key ) ) { return $answer; } $table = self::table(); $now = current_time( 'mysql', true ); $state = in_array( $sign['state'] ?? '', self::STATES, true ) ? $sign['state'] : ''; $nudged = in_array( $sign['nudge_state'] ?? '', array( 'shown', 'clicked' ), true ) ? $sign['nudge_state'] : ''; $row = array( 'updated' => $now, 'page' => mb_substr( (string) strtok( sanitize_text_field( (string) ( $sign['page'] ?? '' ) ), '?#' ), 0, 191 ), 'title' => mb_substr( sanitize_text_field( (string) ( $sign['title'] ?? '' ) ), 0, 191 ), 'form' => '' === $state ? '' : mb_substr( sanitize_text_field( (string) ( $sign['form'] ?? '' ) ), 0, 191 ), 'state' => $state, ); if ( '' !== $nudged ) { $row['nudge'] = mb_substr( sanitize_text_field( (string) ( $sign['nudge'] ?? '' ) ), 0, 100 ); $row['nudge_state'] = $nudged; } $known = $wpdb->get_row( $wpdb->prepare( 'SELECT page, pages, city, push FROM %i WHERE id = %s', $table, $key ) ); if ( $known ) { $row['pages'] = min( 999, (int) $known->pages + ( $known->page !== $row['page'] ? 1 : 0 ) ); // A message from the live screen is handed over once. if ( '' !== (string) $known->push ) { $answer['push'] = (string) $known->push; $row['push'] = ''; } // The city of a visitor who arrived before the location database was ready. if ( '' === $known->city && Settings::get( 'geo_cities' ) ) { $attribution = Attribution::from_request(); $place = Geo::locate( (string) ( $attribution['tz'] ?? '' ) ); if ( '' !== $place['city'] ) { $row = array_merge( $row, $place, array( 'lat' => (float) $place['lat'], 'lon' => (float) $place['lon'], ) ); } } $answer['ok'] = false !== $wpdb->update( $table, $row, array( 'id' => $key ) ); return $answer; } $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE updated < %s', $table, gmdate( 'Y-m-d H:i:s', time() - self::KEEP ) ) ); $attribution = Attribution::from_request(); $touch = $attribution['last'] ?? array(); $visitor = Visitor::columns( (string) ( $attribution['tz'] ?? '' ) ); $answer['ok'] = (bool) $wpdb->insert( $table, array_merge( $row, array( 'id' => $key, 'ref' => strtolower( wp_generate_password( 12, false ) ), 'started' => $now, 'pages' => 1, 'channel' => $touch ? Attribution::channel( $touch ) : 'direct', 'source' => $touch ? mb_substr( Sources::key( Sources::paid_key( Attribution::source( $touch ), Attribution::channel( $touch ) ) ), 0, 191 ) : '(direct)', 'device' => $visitor['device'], 'browser' => $visitor['browser'], 'country' => $visitor['country'], 'city' => $visitor['city'], 'lat' => $visitor['lat'], 'lon' => $visitor['lon'], ) ) ); return $answer; } /** * Messages the live screen can send to a visitor, e.g. the nudges of Dealroot Pro. * * @return array */ public static function pushes() { /** * Filters the messages that can be sent to a visitor from the live screen. The tracker * passes the ID to the page as the "dealroot:push" event, for a script that shows it. * * @param array $pushes ID (letters and digits, up to 20) and label of each message. */ $pushes = (array) apply_filters( 'dealroot_live_pushes', array() ); return array_values( array_filter( array_map( static function ( $push ) { $id = (string) ( $push['id'] ?? '' ); return preg_match( '/^[a-z0-9]{1,20}$/', $id ) ? array( 'id' => $id, 'label' => mb_substr( (string) ( $push['label'] ?? $id ), 0, 80 ), ) : null; }, $pushes ) ) ); } /** * Hands a message to a visitor on their next sign. * * @param string $ref Public key of the visitor on the live screen. * @param string $push Message ID, one of pushes(). * @return bool */ public static function push( $ref, $push ) { global $wpdb; if ( ! preg_match( '/^[a-z0-9]{12}$/', (string) $ref ) || ! in_array( $push, wp_list_pluck( self::pushes(), 'id' ), true ) ) { return false; } return (bool) $wpdb->update( self::table(), array( 'push' => $push ), array( 'ref' => $ref ) ); } /** * Notes that someone watches the live screen, so pages report every few seconds. */ public static function watch() { set_transient( 'dealroot_live_watched', 1, 30 ); } /** * Whether someone watches the live screen. * * @return bool */ public static function watched() { return (bool) get_transient( 'dealroot_live_watched' ); } /** * Visitors with a page open now, the most recent arrivals first. * * @return object[] */ public static function current() { global $wpdb; return (array) $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE updated >= %s ORDER BY started DESC LIMIT 200', self::table(), gmdate( 'Y-m-d H:i:s', time() - self::ACTIVE ) ) ); } /** * Number of visitors on the site now. * * @return int */ public static function count() { global $wpdb; if ( ! self::enabled() ) { return 0; } return (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE updated >= %s', self::table(), gmdate( 'Y-m-d H:i:s', time() - self::ACTIVE ) ) ); } /** * Removes every row, e.g. when the setting is turned off. */ public static function clear() { global $wpdb; $wpdb->query( $wpdb->prepare( 'DELETE FROM %i', self::table() ) ); } }