PluginProbe
Plausible Analytics / trunk
Plausible Analytics vtrunk
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 / Client.php

Client.php in Plausible Analytics trunk, at src/Client.php

454 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Plausible\Analytics\WP;
4
5 use Exception;
6 use Plausible\Analytics\WP\Capabilities as WPCapabilities;
7 use Plausible\Analytics\WP\Admin\Messages;
8 use Plausible\Analytics\WP\Client\ApiException;
9 use Plausible\Analytics\WP\Client\Lib\GuzzleHttp\Client as GuzzleClient;
10 use Plausible\Analytics\WP\Client\Api\DefaultApi;
11 use Plausible\Analytics\WP\Client\Configuration;
12 use Plausible\Analytics\WP\Client\Model\Capabilities;
13 use Plausible\Analytics\WP\Client\Model\CapabilitiesFeatures;
14 use Plausible\Analytics\WP\Client\Model\CustomPropEnableRequestBulkEnable;
15 use Plausible\Analytics\WP\Client\Model\FunnelCreateRequest;
16 use Plausible\Analytics\WP\Client\Model\GoalCreateRequestBulkGetOrCreate;
17 use Plausible\Analytics\WP\Client\Model\GoalListResponse;
18 use Plausible\Analytics\WP\Client\Model\PaymentRequiredError;
19 use Plausible\Analytics\WP\Client\Model\SharedLink;
20 use Plausible\Analytics\WP\Client\Model\UnauthorizedError;
21 use Plausible\Analytics\WP\Client\Model\UnprocessableEntityError;
22
23 /**
24 * This class acts as middleware between our OpenAPI generated API client and our WP plugin and takes care of setting
25 * the required configuration, so we can use the Client in a unified manner.
26 */
27 class Client {
28 /**
29 * @var DefaultApi $api_instance
30 */
31 private $api_instance;
32
33 /**
34 * @var string $domain_key
35 */
36 private $domain_key;
37
38 /**
39 * Set up basic authorization, basic_auth.
40 *
41 * @param string $token Allows specifying the token, e.g., when it's not stored in the DB yet.
42 * @param string $domain_key The domain key to use for this client.
43 */
44 public function __construct( $token = '', $domain_key = 'default' ) {
45 $this->domain_key = $domain_key;
46 $timeout = (float) apply_filters( 'plausible_analytics_api_timeout', 10.0 );
47 $connect_timeout = (float) apply_filters( 'plausible_analytics_api_connect_timeout', 5.0 );
48 $config = new Configuration();
49 $config->setUsername( 'WordPress' )
50 ->setPassword( $token )
51 ->setHost( Helpers::get_hosted_domain_url() );
52 $this->api_instance = new DefaultApi( new GuzzleClient( [ 'timeout' => $timeout, 'connect_timeout' => $connect_timeout ] ), $config );
53 }
54
55 /**
56 * Allows creating Funnels in bulk.
57 *
58 * @param FunnelCreateRequest $funnel
59 *
60 * @return Client\Model\Funnel|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
61 *
62 * @codeCoverageIgnore
63 */
64 public function create_funnel( $funnel ) {
65 try {
66 return $this->api_instance->funnelGetOrCreate( $funnel );
67 } catch ( Exception $e ) {
68 // translators: %s: Error message.
69 $this->send_json_error( $e, __( 'Something went wrong while creating Funnel: %s', 'plausible-analytics' ) );
70 }
71 }
72
73 /**
74 * @param Exception $e
75 * @param string $error_message The human-readable part of the error message, requires a %s at the end!
76 *
77 * @return void
78 *
79 * @codeCoverageIgnore
80 */
81 private function send_json_error( $e, $error_message ) {
82 if ( ! wp_doing_ajax() ) {
83 return;
84 }
85
86 $code = $e->getCode();
87
88 // Any error codes outside the 4xx range should show a generic error.
89 if ( $code <= 399 || $code >= 500 ) {
90 Messages::set_error( __( 'Something went wrong, try again later.', 'plausible-analytics' ) );
91
92 wp_send_json_error( null, $code );
93 }
94
95 $message = $e->getMessage();
96 $response_body = $e->getResponseBody();
97
98 if ( $response_body !== null ) {
99 $response_json = json_decode( $response_body );
100
101 if ( ! empty( $response_json->errors ) ) {
102 $message = '';
103
104 foreach ( $response_json->errors as $error_no => $error ) {
105 $message .= $error->detail;
106
107 if ( $error_no + 1 === count( $response_json->errors ) ) {
108 $message .= '.';
109 } elseif ( count( $response_json->errors ) > 1 ) {
110 $message .= ', ';
111 }
112 }
113 }
114 }
115
116 Messages::set_error( sprintf( $error_message, $message ) );
117
118 $caps = $this->update_capabilities( '', $this->domain_key );
119
120 wp_send_json_error( [ 'capabilities' => $caps ], $code );
121 }
122
123 /**
124 * Stores the capabilities for the currently entered API token in the DB for later use.
125 *
126 * @param string $token
127 * @param string $domain_key
128 *
129 * @return false|array
130 *
131 * @codeCoverageIgnore
132 */
133 private function update_capabilities( $token = '', $domain_key = '' ) {
134 if ( empty( $domain_key ) ) {
135 $domain_key = $this->domain_key;
136 }
137
138 $client_factory = new ClientFactory( $token, $domain_key );
139 /** @var Client $client */
140 $client = $client_factory->build();
141
142 if ( ! $client instanceof Client ) {
143 return false;
144 }
145
146 /** @var Client\Model\CapabilitiesFeatures $features */
147 $features = $client->get_features();
148
149 if ( ! $features ) {
150 return false;
151 }
152
153 $caps = [
154 WPCapabilities::FUNNELS => $features->getFunnels(),
155 WPCapabilities::GOALS => $features->getGoals(),
156 WPCapabilities::PROPS => $features->getProps(),
157 WPCapabilities::REVENUE => $features->getRevenueGoals(),
158 WPCapabilities::STATS => $features->getStatsApi(),
159 ];
160
161 $all_caps = get_option( 'plausible_analytics_api_token_caps', [] );
162
163 /**
164 * @since v2.6.0 Normalize @var $all_caps if the plugin has been configured prior to this version.
165 */
166 if ( ! empty( $all_caps ) && ! is_array( reset( $all_caps ) ) ) {
167 $all_caps = [ 'default' => $all_caps ];
168 }
169
170 $all_caps[ $domain_key ] = $caps;
171
172 update_option( 'plausible_analytics_api_token_caps', $all_caps );
173
174 return $caps;
175 }
176
177 /**
178 * Retrieve Features from the Capabilities object.
179 *
180 * @return false|Client\Model\CapabilitiesFeatures
181 */
182 public function get_features() {
183 $capabilities = $this->get_capabilities();
184
185 if ( $capabilities instanceof Capabilities ) {
186 return $capabilities->getFeatures();
187 }
188
189 return false; // @codeCoverageIgnore
190 }
191
192 /**
193 * Retrieve all capabilities assigned to configured Plugin Token.
194 *
195 * @return bool|Client\Model\Capabilities
196 *
197 * @codeCoverageIgnore
198 */
199 private function get_capabilities() {
200 try {
201 return $this->api_instance->plausibleWebPluginsAPIControllersCapabilitiesIndex();
202 } catch ( \Exception $e ) {
203 return false;
204 }
205 }
206
207 /**
208 * Allows creating Custom Event Goals in bulk.
209 *
210 * @param GoalCreateRequestBulkGetOrCreate $goals
211 *
212 * @return GoalListResponse|PaymentRequiredError|UnauthorizedError|UnprocessableEntityError|void
213 *
214 * @codeCoverageIgnore
215 */
216 public function create_goals( $goals ) {
217 try {
218 return $this->api_instance->goalGetOrCreate( $goals );
219 } catch ( Exception $e ) {
220 // translators: %s: Error message.
221 $this->send_json_error( $e, __( 'Something went wrong while creating Custom Event Goal: %s', 'plausible-analytics' ) );
222 }
223 }
224
225 /**
226 * Create Shared Link in Plausible Dashboard.
227 *
228 * @return void
229 */
230 public function create_shared_link( $key = 'default' ) {
231 $shared_link = (object) [];
232 $result = (object) [];
233
234 try {
235 $result = $this->bulk_create_shared_links();
236 // @codeCoverageIgnoreStart
237 } catch ( Exception $e ) {
238 // translators: %s: Error message.
239 $this->send_json_error( $e, __( 'Something went wrong while creating Shared Link: %s', 'plausible-analytics' ) );
240 // @codeCoverageIgnoreEnd
241 }
242
243 if ( $result instanceof SharedLink ) {
244 $shared_link = $result->getSharedLink();
245 }
246
247 if ( ! empty( $shared_link->getHref() ) ) {
248 Helpers::update_setting( 'shared_link', $shared_link->getHref(), $key );
249 }
250 }
251
252 /**
253 * @return SharedLink|UnauthorizedError|UnprocessableEntityError
254 * @throws ApiException
255 *
256 * @codeCoverageIgnore
257 */
258 public function bulk_create_shared_links() {
259 return $this->api_instance->plausibleWebPluginsAPIControllersSharedLinksCreate(
260 [ 'shared_link' => [ 'name' => 'WordPress - Shared Dashboard', 'password_protected' => false ] ]
261 );
262 }
263
264 /**
265 * Delete a Custom Event Goal by ID.
266 *
267 * @param int $id
268 *
269 * @codeCoverageIgnore
270 */
271 public function delete_goal( $id ) {
272 try {
273 $this->api_instance->plausibleWebPluginsAPIControllersGoalsDelete( $id );
274 } catch ( Exception $e ) {
275 $this->send_json_error(
276 $e,
277 // translators: %s: Error message.
278 __(
279 'Something went wrong while deleting a Custom Event Goal: %s',
280 'plausible-analytics'
281 )
282 );
283 }
284 }
285
286 /**
287 * Enable (or get) a custom property.
288 *
289 * @param CustomPropEnableRequestBulkEnable $enable_request
290 *
291 * @throws PaymentRequiredError|UnauthorizedError|UnprocessableEntityError
292 *
293 * @codeCoverageIgnore
294 */
295 public function enable_custom_property( $enable_request ) {
296 try {
297 $this->api_instance->customPropGetOrEnable( $enable_request );
298 } catch ( Exception $e ) {
299 $this->send_json_error(
300 $e,
301 // translators: %s: Error message.
302 __(
303 'Something went wrong while enabling Pageview Properties: %s',
304 'plausible-analytics'
305 )
306 );
307 }
308 }
309
310 /**
311 * Retrieve the configured Tracker ID and stores it in WP's options table.
312 *
313 * @return string
314 *
315 * @codeCoverageIgnore Because we don't want to test WordPress core functionality.
316 */
317 public function get_tracker_id( $key = 'default' ) {
318 $ids = get_option( 'plausible_analytics_tracker_id', [] );
319
320 if ( ! is_array( $ids ) ) {
321 /** @since v2.6.0 normalization for earlier versions. */
322 $ids = [ 'default' => $ids ];
323 }
324
325 if ( empty( $ids[ $key ] ) ) {
326 $tracker_configuration = $this->get_configuration();
327
328 if ( ! $tracker_configuration instanceof Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration ) {
329 return '';
330 }
331
332 $ids[ $key ] = $tracker_configuration->getId();
333
334 update_option( 'plausible_analytics_tracker_id', $ids );
335 }
336
337 return $ids[ $key ];
338 }
339
340 /**
341 * Retrieve the configured Tracker Script Configuration.
342 *
343 * @return false|Client\Model\TrackerScriptConfigurationTrackerScriptConfiguration
344 *
345 * @codeCoverageIgnore Because we don't want to test the API's response.
346 */
347 private function get_configuration() {
348 try {
349 $configuration = $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationGet();
350
351 return $configuration->getTrackerScriptConfiguration();
352 } catch ( \Exception $e ) {
353 return false;
354 }
355 }
356
357 /**
358 * Update the configured Tracker Script Configuration.
359 *
360 * @param \Plausible\Analytics\WP\Client\Model\TrackerScriptConfigurationUpdateRequest $tracker_script_config_update_request
361 *
362 * @codeCoverageIgnore
363 */
364 public function update_tracker_script_configuration( $tracker_script_config_update_request ) {
365 try {
366 $this->api_instance->plausibleWebPluginsAPIControllersTrackerScriptConfigurationUpdate(
367 $tracker_script_config_update_request
368 );
369 } catch ( Exception $e ) {
370 $this->send_json_error(
371 $e,
372 // translators: %s: Error message.
373 __(
374 'Something went wrong while updating tracker script configuration: %s',
375 'plausible-analytics'
376 )
377 );
378 }
379 }
380
381 /**
382 * Validates the Plugin Token (password) set in the current instance and caches the state to a transient valid for 1 day.
383 *
384 * @return bool
385 */
386 public function validate_api_token() {
387 if ( $this->is_api_token_valid() ) {
388 return true; // @codeCoverageIgnore
389 }
390
391 $features = $this->get_features();
392
393 if ( ! $features instanceof CapabilitiesFeatures ) {
394 return false; // @codeCoverageIgnore
395 }
396
397 $data_domain = $this->get_data_domain();
398 $token = $this->api_instance->getConfig()->getPassword();
399 $is_valid = str_contains( $token, 'plausible-plugin' ) && ! empty( $features->getGoals() ) && $data_domain === Helpers::get_domain();
400
401 /**
402 * Don't cache invalid API tokens.
403 */
404 if ( $is_valid ) {
405 $valid_tokens = get_transient( 'plausible_analytics_valid_token' );
406
407 if ( ! is_array( $valid_tokens ) ) {
408 $valid_tokens = [];
409 }
410
411 $valid_tokens[ $token ] = true;
412
413 set_transient( 'plausible_analytics_valid_token', $valid_tokens, 86400 ); // @codeCoverageIgnore
414
415 $this->update_capabilities( $token ); // @codeCoverageIgnore
416 }
417
418 return $is_valid;
419 }
420
421 /**
422 * Is the currently stored token valid?
423 *
424 * @return bool
425 */
426 public function is_api_token_valid() {
427 $token = $this->api_instance->getConfig()->getPassword();
428 $valid_tokens = get_transient( 'plausible_analytics_valid_token' );
429
430 if ( ! is_array( $valid_tokens ) ) {
431 return false;
432 }
433
434 return isset( $valid_tokens[ $token ] ) && $valid_tokens[ $token ] === true;
435 }
436
437 /**
438 * Retrieve Data Domain property from Capabilities object.
439 *
440 * @return false|string
441 *
442 * @codeCoverageIgnore
443 */
444 public function get_data_domain() {
445 $capabilities = $this->get_capabilities();
446
447 if ( $capabilities instanceof Capabilities ) {
448 return $capabilities->getDataDomain();
449 }
450
451 return false;
452 }
453 }
454