Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tracks
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 7
1332
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
 platform
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 record
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
156
 record_refusal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 comment_posted
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
132
1<?php
2/**
3 * Usage events.
4 *
5 * @package automattic/jetpack-comments
6 */
7
8namespace Automattic\Jetpack\Comments;
9
10use Automattic\Jetpack\Connection\Manager;
11use Automattic\Jetpack\Status;
12use Automattic\Jetpack\Status\Host;
13use Automattic\Jetpack\Terms_Of_Service;
14use Automattic\Jetpack\Tracking;
15
16/**
17 * Records how the form is used, in Tracks. The browser records the rest through the same gate.
18 */
19class Tracks {
20
21    /**
22     * Singleton instance.
23     *
24     * @var Tracks|null
25     */
26    private static $instance = null;
27
28    /**
29     * Register the hooks. Safe to call more than once.
30     *
31     * @return Tracks
32     */
33    public static function init() {
34        if ( null === self::$instance ) {
35            self::$instance = new self();
36        }
37
38        return self::$instance;
39    }
40
41    /**
42     * Count the comments the form posts.
43     */
44    private function __construct() {
45        add_action( 'comment_post', array( $this, 'comment_posted' ), 10, 3 );
46    }
47
48    /**
49     * Whether events may be recorded: always on WordPress.com, on Atomic outside offline mode,
50     * and elsewhere once the owner agreed to Jetpack's terms, which offline mode also fails.
51     *
52     * @return bool
53     */
54    public static function is_enabled() {
55        $platform = self::platform();
56
57        return 'simple' === $platform
58            || ( 'atomic' === $platform && ! ( new Status() )->is_offline_mode() )
59            || ( new Terms_Of_Service() )->has_agreed();
60    }
61
62    /**
63     * The host the event came from.
64     *
65     * @return string `simple`, `atomic` or `self_hosted`.
66     */
67    public static function platform() {
68        $host = new Host();
69
70        if ( $host->is_wpcom_simple() ) {
71            return 'simple';
72        }
73
74        return $host->is_woa_site() ? 'atomic' : 'self_hosted';
75    }
76
77    /**
78     * Record an event. A visitor's is recorded under the site's owner, as Jetpack Forms does, and holds nothing they typed.
79     *
80     * @param string $event_name Full event name, e.g. `jetpack_comments_comment_posted`.
81     * @param array  $properties Event properties.
82     * @return void
83     */
84    public static function record( $event_name, array $properties = array() ) {
85        if ( ! self::is_enabled() ) {
86            return;
87        }
88
89        $properties['blog_id']  = Checkpoint::blog_id();
90        $properties['platform'] = self::platform();
91        $properties['version']  = Comments::PACKAGE_VERSION;
92
93        $user = wp_get_current_user();
94
95        if ( ( new Host() )->is_wpcom_simple() ) {
96            if ( ! $user->exists() ) {
97                $user = get_userdata( (int) wpcom_get_blog_owner( get_current_blog_id() ) );
98            }
99
100            if ( ! function_exists( 'tracks_record_event' ) && function_exists( 'require_lib' ) ) {
101                require_lib( 'tracks/client' );
102            }
103
104            if ( $user && function_exists( 'tracks_record_event' ) ) {
105                tracks_record_event( $user, $event_name, $properties );
106            }
107
108            return;
109        }
110
111        if ( ! $user->exists() ) {
112            $user = get_userdata( (int) ( new Manager() )->get_connection_owner_id() );
113        }
114
115        if ( ! $user ) {
116            return;
117        }
118
119        // Atomic does not always store the agreement Tracking checks for; masterbar works around it the same way.
120        $agreed = static function ( $value, $name ) {
121            return Terms_Of_Service::OPTION_NAME === $name ? true : $value;
122        };
123
124        if ( 'atomic' === $properties['platform'] ) {
125            add_filter( 'jetpack_options', $agreed, 10, 2 );
126        }
127
128        ( new Tracking() )->tracks_record_event( $user, $event_name, $properties );
129
130        remove_filter( 'jetpack_options', $agreed, 10 );
131    }
132
133    /**
134     * Record a comment the form turned away, where a browser on this site sent it. Anything else is a bot,
135     * and would cost an outbound request to record.
136     *
137     * @param string $reason The error code the sign-in was refused with.
138     * @return void
139     */
140    public static function record_refusal( $reason ) {
141        if ( ! empty( $_SERVER['HTTP_ORIGIN'] ) && Checkpoint::is_same_site_request() ) {
142            self::record( 'jetpack_comments_comment_refused', array( 'reason' => $reason ) );
143        }
144    }
145
146    /**
147     * Who posted a comment through the form, and with what.
148     *
149     * @param int        $comment_id  The comment ID.
150     * @param int|string $approved    1, 0, `spam` or `trash`.
151     * @param array      $commentdata Comment data.
152     * @return void
153     */
154    public function comment_posted( $comment_id, $approved, $commentdata = array() ) {
155        // phpcs:disable WordPress.Security.NonceVerification.Missing -- Comment_Form::verify_nonce() checked it; its presence marks a comment this form sent.
156        if ( empty( $_POST[ Comment_Form::NONCE_NAME ] ) ) {
157            return;
158        }
159
160        // A theme drew its own form, so the app never showed on this site.
161        if ( ! empty( $_POST[ Comment_Form::THEME_FORM ] ) ) {
162            self::record(
163                'jetpack_comments_theme_form_posted',
164                array(
165                    'theme'     => get_stylesheet(),
166                    'status'    => (string) $approved,
167                    'post_type' => (string) get_post_type( (int) ( $commentdata['comment_post_ID'] ?? 0 ) ),
168                )
169            );
170            return;
171        }
172
173        $code     = ! empty( $_POST[ Checkpoint::CODE_FIELD ] );
174        $passport = ! empty( $_POST[ Checkpoint::PASSPORT_FIELD ] );
175
176        if ( is_user_logged_in() ) {
177            $identity = 'user';
178        } elseif ( $code || $passport ) {
179            // Checkpoint::admit() turned the comment away unless one of these let the commenter in.
180            $identity = 'wordpress'; // phpcs:ignore WordPress.WP.CapitalPDangit.MisspelledInText -- Commenter.kind in the app.
181        } elseif ( empty( $commentdata['comment_author'] ) && empty( $commentdata['comment_author_email'] ) ) {
182            $identity = 'anonymous';
183        } else {
184            $identity = 'guest';
185        }
186
187        // Paragraphs and list items come with every comment and every list.
188        $content = (string) ( $commentdata['comment_content'] ?? '' );
189        preg_match_all( '#<!-- wp:([a-z0-9-]+(?:/[a-z0-9-]+)?)#', $content, $names );
190        $blocks = array_diff( array_unique( $names[1] ), array( 'paragraph', 'list-item' ) );
191        sort( $blocks );
192
193        self::record(
194            'jetpack_comments_comment_posted',
195            array(
196                'identity'           => $identity,
197                'new_sign_in'        => $code && ! is_user_logged_in(),
198                'is_reply'           => ! empty( $commentdata['comment_parent'] ),
199                'editor'             => has_blocks( $content ) ? 'blocks' : 'textarea',
200                'blocks'             => implode( ',', $blocks ),
201                'subscribe_comments' => ! empty( $_POST['subscribe_comments'] ) || ! empty( $_POST['subscribe'] ),
202                'subscribe_blog'     => ! empty( $_POST['subscribe_blog'] ),
203                'remember_details'   => ! empty( $_POST['wp-comment-cookies-consent'] ),
204                'status'             => (string) $approved,
205                'post_type'          => (string) get_post_type( (int) ( $commentdata['comment_post_ID'] ?? 0 ) ),
206            )
207        );
208        // phpcs:enable WordPress.Security.NonceVerification.Missing
209    }
210}