PluginProbe
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity / 3.3.9
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity v3.3.9
3.3.9 3.3.8 trunk 1.0 1.1.0 1.10.0 1.11.0 1.11.1 1.12.0 1.13.0 1.14.0 1.15.0 1.16.0 1.17.0 1.17.1 1.18.0 1.19.0 1.2.0 1.20.0 1.20.1 1.3.0 1.3.1 1.4.0 1.5.0 1.6.0 All 67 releases
logtivity / Base / Logtivity_Api.php

Logtivity_Api.php in Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity 3.3.9, at Base/Logtivity_Api.php

440 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @package Logtivity
5 * @contact logtivity.io, [email protected]
6 * @copyright 2024-2026 Logtivity. All rights reserved
7 * @license https://www.gnu.org/licenses/gpl.html GNU/GPL
8 *
9 * This file is part of Logtivity.
10 *
11 * Logtivity is free software: you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation, either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * Logtivity is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 *
21 * You should have received a copy of the GNU General Public License
22 * along with Logtivity. If not, see <https://www.gnu.org/licenses/>.
23 */
24
25 class Logtivity_Api
26 {
27 public const CURL_TIMEOUT = 28;
28
29 /**
30 * @var array
31 */
32 protected static $responseData = [
33 'code' => null,
34 'message' => null,
35 'error' => null,
36 'body' => null,
37 ];
38
39 /**
40 * Option class to access the plugin settings
41 *
42 * @var Logtivity_Options
43 */
44 protected Logtivity_Options $options;
45
46 /**
47 * We generally don't want to wait for a response from the API.
48 *
49 * @var bool
50 */
51 protected bool $waitForResponse = false;
52
53 /**
54 * If the currrent status is not 'fail' or 'success'
55 * attempt to send anyway
56 *
57 * @var bool
58 */
59 protected bool $ignoreStatus = false;
60
61 /**
62 * The API Key for either the site or team
63 *
64 * @var ?string
65 */
66 protected ?string $api_key = null;
67
68 public function __construct()
69 {
70 $this->options = new Logtivity_Options();
71 }
72
73 /**
74 * @return $this
75 */
76 public function ignoreStatus(): self
77 {
78 $this->ignoreStatus = true;
79
80 return $this;
81 }
82
83 /**
84 * @param string $url
85 * @param array $body
86 *
87 * @return ?array
88 */
89 public function post(string $url, array $body): ?array
90 {
91 return $this->makeRequest($url, $body);
92 }
93
94 /**
95 * Make a request to the Logtivity API
96 *
97 * @param string $url
98 * @param ?array $body
99 * @param string $method
100 *
101 * @return ?array
102 */
103 public function makeRequest(string $url, ?array $body = [], string $method = 'POST'): ?array
104 {
105 $body = $body ?: [];
106
107 $body['occurred_at'] = time();
108
109 if ($this->options->urlHash() == false) {
110 $this->options->update(['logtivity_url_hash' => md5(home_url())], false);
111 }
112
113 if ($this->ready()) {
114 if ($this->getOption('logtivity_app_verify_url')) {
115 $body['site_hash'] = $this->options->urlHash();
116 }
117
118 $waitForResponse = $this->waitForResponse || $this->options->shouldLogLatestResponse();
119
120 $request = [
121 'method' => $method,
122 'timeout' => ($waitForResponse ? 6 : 0.01),
123 'blocking' => $waitForResponse,
124 'redirection' => 5,
125 'httpversion' => '1.0',
126 'headers' => [
127 'Authorization' => 'Bearer ' . $this->getApiKey(),
128 ],
129 'body' => $body,
130 'cookies' => [],
131 ];
132
133 /**
134 * This request must not be cached. It writes a log entry to the API rather
135 * than reading a cacheable resource, so a cached response would silently
136 * drop logs. The default path is also non-blocking with a 0.01s timeout,
137 * so it does not hold up the page load.
138 *
139 * Settings check-ins are already rate limited by
140 * Logtivity_Options::shouldCheckInWithApi(), which gates on the
141 * logtivity_last_settings_check_in_at option.
142 *
143 * @TODO: Switch to Logtivity_Response class to get standardized responses
144 */
145 $response = wp_remote_request($this->getEndpoint($url), $request);
146 if ($waitForResponse && $this->notUpdatingWidgetInCustomizer()) {
147 // We waited and received a response
148 if ($response instanceof WP_Error) {
149 $responseData = array_merge(static::$responseData, [
150 'code' => 500,
151 'message' => $response->get_error_code(),
152 'error' => $response->get_error_message() ?: $response->get_error_code(),
153 'body' => get_object_vars($response),
154 ]);
155
156 } else {
157 $responseCode = wp_remote_retrieve_response_code($response);
158 $responseMessage = wp_remote_retrieve_response_message($response);
159 $responseBody = json_decode(wp_remote_retrieve_body($response), true);
160
161 if ($responseCode < 400) {
162 $responseError = $responseBody['error'] ?? null;
163 } else {
164 $responseError = $responseMessage;
165 $responseMessage = (($responseBody['message'] ?? $responseMessage) ?: 'Unknown error');
166 }
167 $responseData = [
168 'code' => $responseCode,
169 'message' => $responseMessage,
170 'error' => $responseError,
171 'body' => $responseBody,
172 ];
173 }
174
175 $newStatus = 'success';
176 if ($responseData['code']) {
177 if ($responseData['code'] < 400) {
178 // Successful request, check if api is telling us to pause
179 if ($responseData['error']) {
180 $newStatus = 'paused';
181 }
182
183 $body = json_decode($response['body'] ?? 'null', true);
184 $this->updateSettings($body['settings'] ?? []);
185
186 } else {
187 if ($this->getCurlError($responseData['error']) != static::CURL_TIMEOUT) {
188 // Something other than a timeout went wrong with the request
189 $newStatus = 'fail';
190 }
191
192 $this->updateLastCheckin();
193 }
194
195 $this->options->update([
196 'logtivity_api_key_check' => $newStatus,
197 'logtivity_latest_response' => array_merge(
198 ['date' => date('Y-m-d H:i:s')],
199 $responseData
200 ),
201 ],
202 false
203 );
204
205 return $responseData;
206 }
207 }
208 }
209
210 return null;
211 }
212
213 /**
214 * @return bool
215 */
216 protected function ready(): bool
217 {
218 return logtivity_url_environment_mismatch() == false
219 && ($this->getApiKey())
220 && logtivity_has_site_url_changed() == false
221 && (
222 $this->ignoreStatus
223 || $this->getOption('logtivity_api_key_check') == 'success'
224 || $this->options->shouldCheckInWithApi()
225 );
226 }
227
228 /**
229 * @return ?string
230 */
231 public function getApiKey(): ?string
232 {
233 if ($this->api_key == false) {
234 $this->api_key = $this->options->getApiKey();
235 }
236
237 return $this->api_key;
238 }
239
240 /**
241 * @param string $apikey
242 *
243 * @return $this
244 */
245 public function setApiKey(string $apikey): self
246 {
247 $this->api_key = $apikey;
248
249 return $this;
250 }
251
252 /**
253 * @param ?string $key
254 *
255 * @return Logtivity_Options|mixed
256 */
257 public function getOption(?string $key = null)
258 {
259 if ($key) {
260 return $this->options->getOption($key);
261 }
262
263 return $this->options;
264 }
265
266 /**
267 * @param string $endpoint
268 *
269 * @return string
270 */
271 public function getEndpoint(string $endpoint): string
272 {
273 return logtivity_get_api_url() . $endpoint;
274 }
275
276 /**
277 * You cannot call an extra update_option during a widget update so we make
278 * sure not to log the most recent log response in this case.
279 *
280 * @return bool
281 */
282 protected function notUpdatingWidgetInCustomizer(): bool
283 {
284 $customize = sanitize_text_field($_POST['wp_customize'] ?? null);
285 $action = sanitize_text_field($_POST['action'] ?? null);
286
287 return ($action == 'update-widget' && $customize == 'on') == false;
288 }
289
290 /**
291 * @param array $settings
292 *
293 * @return void
294 */
295 public function updateSettings(array $settings): void
296 {
297 if ($settings) {
298 $verifiedSettings = [];
299 foreach ($settings as $key => $value) {
300 if ($key == 'disabled_logs') {
301 $key = 'global_' . $key;
302 }
303 $verifiedSettings['logtivity_' . $key] = is_null($value) ? '' : $value;
304 }
305
306 $this->options->update($verifiedSettings, false);
307 }
308
309 $this->updateLastCheckin();
310 }
311
312 /**
313 * @return void
314 */
315 protected function updateLastCheckin(): void
316 {
317 update_option('logtivity_last_settings_check_in_at', ['date' => date('Y-m-d H:i:s')]);
318 }
319
320 /**
321 * @param ?string $message
322 *
323 * @return int
324 */
325 protected function getCurlError(?string $message): ?int
326 {
327 preg_match('/curl\s+error\s+(\d+)/i', $message, $matches);
328
329 return $matches[1] ?? null;
330 }
331
332 /**
333 * @return ?array
334 */
335 public function getLatestResponse(): ?array
336 {
337 $response = $this->getOption('logtivity_latest_response');
338
339 return $response ?: null;
340 }
341
342 /**
343 * @return ?Exception
344 */
345 public function getLastError(): ?Exception
346 {
347 $latestResponse = $this->getLatestResponse();
348
349 $code = $latestResponse['code'] ?? null;
350
351 if ($code >= 400 && isset($latestResponse['body']['message'])) {
352 $message = $latestResponse['body']['message'];
353
354 } elseif (isset($latestResponse['body']['errors'])) {
355 $message = array_shift($latestResponse['body']['errors']);
356 while (is_array($message)) {
357 $message = array_shift($message);
358 }
359
360 } elseif (isset($latestResponse['body']['error'])) {
361 $message = $latestResponse['body']['error'];
362 } else {
363 $message = $latestResponse['error'] ?? $latestResponse['message'] ?? 'Unknown error';
364 }
365
366 if (($code == false || $message == false) && $this->getApiKey()) {
367 $message = 'Not connected. Please check API Key';
368 }
369
370 return new Exception($message, $code);
371 }
372
373 /**
374 * @param string $url
375 * @param ?array $body
376 *
377 * @return ?array
378 */
379 public function get(string $url, ?array $body = []): ?array
380 {
381 return $this->waitForResponse()->makeRequest($url, $body, 'GET');
382 }
383
384 /**
385 * @return $this
386 */
387 public function waitForResponse(): self
388 {
389 $this->waitForResponse = true;
390
391 return $this;
392 }
393
394 /**
395 * @TODO: This method is duplicated in the App.
396 *
397 * @return ?string
398 */
399 public function getConnectionMessage(): ?string
400 {
401 $status = $this->getConnectionStatus();
402
403 switch ($status) {
404 case 'success':
405 $message = 'Connected';
406 break;
407
408 case 'paused':
409 case 'fail':
410 $error = $this->getLastError();
411 $message = $error
412 ? sprintf('Disconnected (%s - %s)', $error->getCode(), $error->getMessage())
413 : null;
414 break;
415
416 case 'staging':
417 $message = 'Connection disabled. Site url has changed.';
418 break;
419
420 default:
421 $message = $this->getApiKey() ? null : 'API Key has not been set';
422 break;
423 }
424
425 return $message ?: 'Unknown Status';
426 }
427
428 /**
429 * @return ?string
430 */
431 public function getConnectionStatus(): ?string
432 {
433 if (logtivity_has_site_url_changed()) {
434 return 'staging';
435 }
436
437 return $this->getApiKey() ? $this->getOption('logtivity_api_key_check') : null;
438 }
439 }
440