Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.71% covered (danger)
35.71%
15 / 42
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Tracks_Event
35.71% covered (danger)
35.71%
15 / 42
14.29% covered (danger)
14.29%
1 / 7
114.91
0.00% covered (danger)
0.00%
0 / 1
 __construct
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
3.33
 record
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate_and_sanitize
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
5.31
 build_pixel_url
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 event_name_is_valid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prop_name_is_valid
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 scrutinize_event_names
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * Class Jetpack_Tracks_Event. Legacy.
4 *
5 * @package automattic/jetpack-sync
6 */
7
8/*
9 * Example Usage:
10```php
11    require_once( dirname(__FILE__) . 'path/to/tracks/class-jetpack-tracks-event.php' );
12
13    $event = new Jetpack_Tracks_Event( array(
14        '_en'        => $event_name,       // required
15        '_ui'        => $user_id,          // required unless _ul is provided
16        '_ul'        => $user_login,       // required unless _ui is provided
17
18        // Optional, but recommended
19        '_via_ip'    => $client_ip,        // for geo, etc.
20
21        // Possibly useful to set some context for the event
22        '_via_ua'    => $client_user_agent,
23        '_via_url'   => $client_url,
24        '_via_ref'   => $client_referrer,
25
26        // For user-targeted tests
27        'abtest_name'        => $abtest_name,
28        'abtest_variation'   => $abtest_variation,
29
30        // Your application-specific properties
31        'custom_property'    => $some_value,
32    ) );
33
34    if ( is_wp_error( $event->error ) ) {
35        // Handle the error in your app
36    }
37
38    $bump_and_redirect_pixel = $event->build_signed_pixel_url();
39```
40 */
41
42/**
43 * Class Jetpack_Tracks_Event
44 */
45#[AllowDynamicProperties]
46class Jetpack_Tracks_Event {
47    const EVENT_NAME_REGEX = '/^(([a-z0-9]+)_){2}([a-z0-9_]+)$/';
48    const PROP_NAME_REGEX  = '/^[a-z_][a-z0-9_]*$/';
49
50    /**
51     * Tracks Event Error.
52     *
53     * @var mixed Error.
54     */
55    public $error;
56
57    /**
58     * Jetpack_Tracks_Event constructor.
59     *
60     * @param object $event Tracks event.
61     */
62    public function __construct( $event ) {
63        $_event = self::validate_and_sanitize( $event );
64        if ( is_wp_error( $_event ) ) {
65            $this->error = $_event;
66            return;
67        }
68
69        foreach ( $_event as $key => $value ) {
70            $this->{$key} = $value;
71        }
72    }
73
74    /**
75     * Record a track event.
76     */
77    public function record() {
78        return Jetpack_Tracks_Client::record_event( $this );
79    }
80
81    /**
82     * Annotate the event with all relevant info.
83     *
84     * @param  mixed $event Object or (flat) array.
85     * @return mixed        The transformed event array or WP_Error on failure.
86     */
87    public static function validate_and_sanitize( $event ) {
88        $event = (object) $event;
89
90        // Required.
91        if ( ! $event->_en ) {
92            return new WP_Error( 'invalid_event', 'A valid event must be specified via `_en`', 400 );
93        }
94
95        // delete non-routable addresses otherwise geoip will discard the record entirely.
96        if ( property_exists( $event, '_via_ip' ) && preg_match( '/^192\.168|^10\./', $event->_via_ip ) ) {
97            unset( $event->_via_ip );
98        }
99
100        $validated = array(
101            'browser_type' => Jetpack_Tracks_Client::BROWSER_TYPE,
102            '_aua'         => Jetpack_Tracks_Client::get_user_agent(),
103        );
104
105        $_event = (object) array_merge( (array) $event, $validated );
106
107        // If you want to block property names, do it here.
108
109        // Make sure we have an event timestamp.
110        if ( ! isset( $_event->_ts ) ) {
111            $_event->_ts = Jetpack_Tracks_Client::build_timestamp();
112        }
113
114        return $_event;
115    }
116
117    /**
118     * Build a pixel URL that will send a Tracks event when fired.
119     * On error, returns an empty string ('').
120     *
121     * @return string A pixel URL or empty string ('') if there were invalid args.
122     */
123    public function build_pixel_url() {
124        if ( $this->error ) {
125            return '';
126        }
127
128        $args = get_object_vars( $this );
129
130        // Request Timestamp and URL Terminator must be added just before the HTTP request or not at all.
131        unset( $args['_rt'] );
132        unset( $args['_'] );
133
134        $validated = self::validate_and_sanitize( $args );
135
136        if ( is_wp_error( $validated ) ) {
137            return '';
138        }
139
140        return Jetpack_Tracks_Client::PIXEL . '?' . http_build_query( $validated );
141    }
142
143    /**
144     * Validate the event name.
145     *
146     * @param string $name Event name.
147     * @return false|int
148     */
149    public static function event_name_is_valid( $name ) {
150        return preg_match( self::EVENT_NAME_REGEX, $name );
151    }
152
153    /**
154     * Validates prop name
155     *
156     * @param string $name Property name.
157     *
158     * @return false|int Truthy value.
159     */
160    public static function prop_name_is_valid( $name ) {
161        return preg_match( self::PROP_NAME_REGEX, $name );
162    }
163
164    /**
165     * Scrutinize event name.
166     *
167     * @param object $event Tracks event.
168     */
169    public static function scrutinize_event_names( $event ) {
170        if ( ! self::event_name_is_valid( $event->_en ) ) {
171            return;
172        }
173
174        $whitelisted_key_names = array(
175            'anonId',
176            'Browser_Type',
177        );
178
179        foreach ( array_keys( (array) $event ) as $key ) {
180            if ( in_array( $key, $whitelisted_key_names, true ) ) {
181                continue;
182            }
183            if ( ! self::prop_name_is_valid( $key ) ) {
184                return;
185            }
186        }
187    }
188}