PluginProbe
Plausible Analytics / 2.6.1
Plausible Analytics v2.6.1
2.6.1 2.6.0 trunk 1.0.0 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 All 52 releases
plausible-analytics / src / Proxy.php

Proxy.php in Plausible Analytics 2.6.1, at src/Proxy.php

255 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Plausible Analytics | Proxy.
4 *
5 * @since 1.3.0
6 * @package WordPress
7 * @subpackage Plausible Analytics
8 * @copyright This code was copied from CAOS Pro, created by:
9 * @author Daan van den Bergh
10 * https://daan.dev/wordpress/caos-pro/
11 */
12
13 namespace Plausible\Analytics\WP;
14
15 use Exception;
16 use WP_Error;
17 use WP_HTTP_Response;
18 use WP_REST_Request;
19 use WP_REST_Server;
20
21 class Proxy {
22 /**
23 * Proxy IP Headers used to detect the visitors IP prior to sending the data to Plausible's Measurement Protocol.
24 *
25 * @see https://support.cloudflare.com/hc/en-us/articles/200170986-How-does-Cloudflare-handle-HTTP-Request-headers-
26 * @var array
27 * For CloudFlare compatibility HTTP_CF_CONNECTING_IP has been added.
28 */
29 const PROXY_IP_HEADERS = [
30 'HTTP_CF_CONNECTING_IP',
31 'HTTP_X_FORWARDED_FOR',
32 'REMOTE_ADDR',
33 'HTTP_CLIENT_IP',
34 ];
35
36 /**
37 * API namespace
38 *
39 * @var string
40 */
41 private $namespace = '';
42
43 /**
44 * API base
45 *
46 * @var string
47 */
48 private $base = '';
49
50 /**
51 * Endpoint
52 *
53 * @var string
54 */
55 private $endpoint = '';
56
57 /**
58 * Build properties.
59 *
60 * @return void
61 * @throws Exception
62 */
63 public function __construct( $init = true ) {
64 $this->namespace = Helpers::get_proxy_resource( 'namespace' ) . '/v1';
65 $this->base = Helpers::get_proxy_resource( 'base' );
66 $this->endpoint = Helpers::get_proxy_resource( 'endpoint' );
67
68 $this->init( $init );
69 }
70
71 /**
72 * Actions
73 *
74 * @return void
75 */
76 private function init( $init ) {
77 if ( ! $init ) {
78 return;
79 }
80
81 $settings = [];
82
83 if ( array_key_exists( 'option_name', $_POST ) && $_POST[ 'option_name' ] == 'proxy_enabled' && array_key_exists( 'option_value', $_POST ) && $_POST[ 'option_value' ] == 'on' ) {
84 $settings[ 'proxy_enabled' ] = 'on'; // @codeCoverageIgnore
85 }
86
87 // No need to continue if Proxy isn't enabled .
88 if ( Helpers::proxy_enabled( $settings ) ) {
89 add_action( 'rest_api_init', [ $this, 'register_route' ] );
90 }
91
92 add_filter( 'rest_post_dispatch', [ $this, 'force_http_response_code' ], null, 3 );
93 }
94
95 /**
96 * A public wrapper to programmatically send hits to the Plausible API.
97 *
98 * @see https://plausible.io/docs/events-api
99 *
100 * @param string $name Name of the event, defaults to 'pageview', all other names are treated as custom events by the API.
101 * @param string $domain Domain of the site in Plausible where the event should be registered.
102 * @param string $url URL of the page where the event was triggered.
103 * @param array $props Custom properties for the event.
104 *
105 * @return array|WP_Error
106 */
107 public function do_request( $name = 'pageview', $domain = '', $url = '', $props = [] ) {
108 $request = new \WP_REST_Request( 'POST', "/$this->namespace/v1/$this->base/$this->endpoint" );
109 $body = [
110 'n' => $name,
111 'd' => $domain ?: Helpers::get_domain(),
112 'u' => $url ?: wp_get_referer(),
113 ];
114
115 // URL is required, so if no $url was set and no referer was found, attempt to create it from the REQUEST_URI server variable.
116 if ( empty( $body[ 'u' ] ) ) {
117 $body[ 'u' ] = $this->generate_event_url(); // @codeCoverageIgnore
118 }
119
120 // Revenue events use a different approach.
121 if ( isset( $props[ 'revenue' ] ) ) {
122 $body[ 'revenue' ] = reset( $props ); // @codeCoverageIgnore
123 } elseif ( ! empty( $props ) ) {
124 $body[ 'p' ] = $props; // @codeCoverageIgnore
125 }
126
127 $request->set_body( wp_json_encode( $body ) );
128
129 return $this->send_event( $request );
130 }
131
132 /**
133 * Attempts to generate the Event URL from available resources.
134 *
135 * @return string
136 */
137 public function generate_event_url() {
138 $url = '';
139 $parts = wp_parse_url( $_SERVER[ 'REQUEST_URI' ] );
140 $home_url_parts = wp_parse_url( get_home_url() );
141
142 if ( isset( $home_url_parts[ 'scheme' ] ) && isset( $home_url_parts[ 'host' ] ) && isset( $parts[ 'path' ] ) ) {
143 $url = $home_url_parts[ 'scheme' ] . '://' . $home_url_parts [ 'host' ] . $parts[ 'path' ];
144 }
145
146 return $url;
147 }
148
149 /**
150 * Formats and sends $request to the Plausible API.
151 *
152 * @return array|WP_Error
153 */
154 public function send_event( $request ) {
155 $params = $request->get_body();
156
157 $ip = $this->get_user_ip_address();
158 $url = 'https://plausible.io/api/event';
159 $ua = ! empty ( $_SERVER[ 'HTTP_USER_AGENT' ] ) ? wp_kses( $_SERVER[ 'HTTP_USER_AGENT' ], 'strip' ) : '';
160
161 return wp_remote_post(
162 $url,
163 [
164 'user-agent' => $ua,
165 'headers' => [
166 'X-Forwarded-For' => $ip,
167 'Content-Type' => 'application/json',
168 ],
169 'body' => wp_kses_no_null( $params ),
170 ]
171 );
172 }
173
174 /**
175 * @return string
176 *
177 * @codeCoverageIgnore
178 */
179 private function get_user_ip_address() {
180 $ip = '';
181
182 foreach ( self::PROXY_IP_HEADERS as $header ) {
183 if ( $this->header_exists( $header ) ) {
184 $ip = wp_kses( $_SERVER[ $header ], 'strip' );
185
186 if ( strpos( $ip, ',' ) !== false ) {
187 $ip = explode( ',', $ip );
188
189 return $ip[ 0 ];
190 }
191
192 return $ip;
193 }
194 }
195
196 return $ip;
197 }
198
199 /**
200 * Checks if a HTTP header is set and is not empty.
201 *
202 * @param mixed $global
203 *
204 * @return bool
205 */
206 private function header_exists( $global ) {
207 return ! empty( $_SERVER[ $global ] );
208 }
209
210 /**
211 * Register the API route.
212 *
213 * @return void
214 *
215 * @codeCoverageIgnore Because we have no way of knowing if the API works in integration tests.
216 */
217 public function register_route() {
218 register_rest_route(
219 $this->namespace,
220 '/' . $this->base . '/' . $this->endpoint,
221 [
222 [
223 'methods' => 'POST',
224 'callback' => [ $this, 'send_event' ],
225 // There's no reason not to allow access to this API.
226 'permission_callback' => '__return_true',
227 ],
228 'schema' => null,
229 ]
230 );
231 }
232
233 /**
234 * Make sure our response code is returned, instead of the default 200 on success.
235 *
236 * @param WP_HTTP_Response $response
237 * @param WP_REST_Server $server
238 * @param WP_REST_Request $request
239 *
240 * @return WP_HTTP_Response
241 *
242 * @codeCoverageIgnore
243 */
244 public function force_http_response_code( $response, $server, $request ) {
245 if ( strpos( $request->get_route(), $this->namespace ) === false ) {
246 return $response; // @codeCoverageIgnore
247 }
248
249 $response_code = wp_remote_retrieve_response_code( $response->get_data() );
250 $response->set_status( $response_code );
251
252 return $response;
253 }
254 }
255