Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.10% covered (success)
93.10%
27 / 29
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Visitor
93.10% covered (success)
93.10%
27 / 29
66.67% covered (warning)
66.67%
2 / 3
17.09
0.00% covered (danger)
0.00%
0 / 1
 get_ip
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
10
 is_automattician_feature_flags_only
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_tracking_automattician
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
6.60
1<?php
2/**
3 * Status and information regarding the site visitor.
4 *
5 * @package automattic/jetpack-status
6 */
7
8namespace Automattic\Jetpack\Status;
9
10use Automattic\Jetpack\IP\Utils as IP_Utils;
11
12/**
13 * Visitor class.
14 */
15class Visitor {
16
17    /**
18     * Gets current user IP address.
19     *
20     * Only a value that parses as an IP address is returned. With `$check_all_headers`, the
21     * forwarded headers are tried in order, a comma-separated list yields its first valid entry,
22     * and a header holding no valid address is skipped.
23     *
24     * A site with a trusted header configured does not use that sweep at all. Brute force
25     * protection stores which header carries the visitor address, how far back to count in its
26     * list, and whether that list runs in reverse, all worked out for this site's own proxy
27     * setup. Once that answer exists it is the answer, including when the request did not carry
28     * the header — `IP\Utils::get_ip()` falls back to `REMOTE_ADDR` itself in that case. Reading
29     * the sweep afterwards would let a request that simply omits the trusted header pick its own
30     * address out of a header it fully controls, and would hand Jetpack two answers for one
31     * request, since brute force protection resolves the same visitor through `IP\Utils`.
32     *
33     * That is a guarantee about which source is consulted, not about the address itself: it
34     * still assumes the proxy named in the stored configuration rewrites the header. A request
35     * reaching the origin directly can present that header itself.
36     *
37     * The address is normalized by `IP\Utils::clean_ip()`: it is lowercased, anything following an
38     * " unless " separator is dropped, and a port suffix, IPv6 brackets, or an `::ffff:` IPv4
39     * mapping are reduced to the bare address. Code comparing this value against a stored or
40     * configured address should normalize that address the same way.
41     *
42     * @param  bool $check_all_headers Check all headers? Default is `false`.
43     *
44     * @return string Current user IP address, or an empty string if no valid address could be determined.
45     */
46    public function get_ip( $check_all_headers = false ) {
47        if ( $check_all_headers ) {
48            $trusted_header_data = get_site_option( 'trusted_ip_header' );
49            if ( isset( $trusted_header_data->trusted_header ) ) {
50                $trusted_ip = IP_Utils::get_ip();
51                return false !== $trusted_ip ? $trusted_ip : '';
52            }
53
54            foreach ( array(
55                'HTTP_CF_CONNECTING_IP',
56                'HTTP_CLIENT_IP',
57                'HTTP_X_FORWARDED_FOR',
58                'HTTP_X_FORWARDED',
59                'HTTP_X_CLUSTER_CLIENT_IP',
60                'HTTP_FORWARDED_FOR',
61                'HTTP_FORWARDED',
62                'HTTP_VIA',
63            ) as $key ) {
64                if ( empty( $_SERVER[ $key ] ) ) {
65                    continue;
66                }
67                // Proxies append to the list, so the leftmost entry is the client.
68                foreach ( explode( ',', (string) wp_unslash( $_SERVER[ $key ] ) ) as $candidate ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each entry is validated by clean_ip() below.
69                    $ip = IP_Utils::clean_ip( $candidate );
70                    if ( false !== $ip ) {
71                        return $ip;
72                    }
73                }
74            }
75        }
76
77        $ip = empty( $_SERVER['REMOTE_ADDR'] ) ? false : IP_Utils::clean_ip( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- clean_ip() validates it.
78        return false !== $ip ? $ip : '';
79    }
80
81    /**
82     * Simple gate check for a11n feature testing purposes using AT_PROXIED_REQUEST constant.
83     * IMPORTANT: Only use it for internal feature test purposes, not authorization.
84     *
85     * The goal of this function is to help us gate features by using a similar function name
86     * we find on simple sites: is_automattician().
87     *
88     * @return bool True if the current request is PROXIED, false otherwise.
89     */
90    public function is_automattician_feature_flags_only() {
91        return ( defined( 'AT_PROXIED_REQUEST' ) && AT_PROXIED_REQUEST );
92    }
93
94    /**
95     * Whether the current request should be attributed to an Automattician in analytics.
96     *
97     * True for an identified Automattician on WordPress.com Simple, and for A8C-proxied
98     * requests on both Simple and WoA. Use it to tag Tracks events as internal traffic so
99     * it can be filtered out of product reporting — this matters most for newly launched
100     * features, where a small amount of internal testing is a large share of the totals
101     * and there is no way to separate it after the fact.
102     *
103     * IMPORTANT: Reporting signal only, never authorization. A proxied request says
104     * something about where the request came from, not who the user is.
105     *
106     * @since 6.4.0
107     *
108     * @return bool True if the request looks like Automattician traffic, false otherwise.
109     */
110    public function is_tracking_automattician() {
111        // Identified Automattician on WordPress.com Simple.
112        if ( function_exists( 'is_automattician' ) && \is_automattician() ) {
113            return true;
114        }
115
116        // Proxied A8C request on WordPress.com Simple.
117        if ( function_exists( 'wpcom_is_proxied_request' ) && \wpcom_is_proxied_request() ) {
118            return true;
119        }
120
121        // Proxied A8C request on WoA.
122        return $this->is_automattician_feature_flags_only();
123    }
124}