'POST', 'callback' => array( 'Visitrix_Tracker', 'collect' ), // Public endpoint by design: anonymous visitors send beacons (works with page caching). 'permission_callback' => '__return_true', ) ); register_rest_route( 'visitrix/v1', '/online', array( 'methods' => 'GET', 'callback' => static function () { return rest_ensure_response( array( 'online' => Visitrix_Stats::online() ) ); }, 'permission_callback' => static function () { return current_user_can( Visitrix_Settings::view_capability() ); }, ) ); register_rest_route( 'visitrix/v1', '/scrollmap', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'scrollmap' ), 'permission_callback' => static function () { return current_user_can( Visitrix_Settings::view_capability() ); }, 'args' => array( 'from' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( __CLASS__, 'valid_date' ), ), 'to' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( __CLASS__, 'valid_date' ), ), 'path' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => static function ( $value ) { $value = substr( (string) $value, 0, 255 ); return ( '' === $value || '/' === $value[0] ) ? $value : ''; }, ), 'device' => array( 'type' => 'string', 'default' => '', 'enum' => array( '', 'desktop', 'mobile', 'tablet' ), ), ), ) ); register_rest_route( 'visitrix/v1', '/stats', array( 'methods' => 'GET', 'callback' => array( __CLASS__, 'stats' ), 'permission_callback' => static function () { return current_user_can( Visitrix_Settings::view_capability() ); }, 'args' => array( 'from' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( __CLASS__, 'valid_date' ), ), 'to' => array( 'required' => true, 'type' => 'string', 'validate_callback' => array( __CLASS__, 'valid_date' ), ), ), ) ); } /** * Y-m-d validator. * * @param mixed $value Value. * @return bool */ public static function valid_date( $value ) { if ( ! is_string( $value ) || ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m ) ) { return false; } return checkdate( (int) $m[2], (int) $m[3], (int) $m[1] ); } /** * Scroll map callback: pages with scroll data and the map of the selected page. * * @param WP_REST_Request $request Request. * @return WP_REST_Response */ public static function scrollmap( WP_REST_Request $request ) { $from = $request->get_param( 'from' ); $to = $request->get_param( 'to' ); if ( $from > $to ) { list( $from, $to ) = array( $to, $from ); } $start = $from . ' 00:00:00'; $end = $to . ' 23:59:59'; $pages = Visitrix_Stats::scroll_pages( $start, $end ); $path = (string) $request->get_param( 'path' ); if ( '' === $path && $pages ) { $path = $pages[0]['path']; } return rest_ensure_response( array( 'pages' => $pages, 'path' => $path, 'map' => '' === $path ? array( 'total' => 0, 'levels' => array_fill( 0, 10, 0 ) ) : Visitrix_Stats::scroll_map( $path, (string) $request->get_param( 'device' ), $start, $end ), ) ); } /** * Stats callback. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public static function stats( WP_REST_Request $request ) { $from = $request->get_param( 'from' ); $to = $request->get_param( 'to' ); if ( $from > $to ) { list( $from, $to ) = array( $to, $from ); } $tz = wp_timezone(); $days = ( new DateTimeImmutable( $from, $tz ) )->diff( new DateTimeImmutable( $to, $tz ) )->days; if ( $days > self::MAX_DAYS ) { return new WP_Error( 'visitrix_range', __( 'The selected range is too long.', 'visitrix' ), array( 'status' => 400 ) ); } return rest_ensure_response( Visitrix_Stats::report( $from, $to ) ); } }