PluginProbe
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity / 3.3.10
Activity Logs, User Activity Tracking, Multisite Activity Log from Logtivity v3.3.10
3.3.10 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 All 68 releases
← All changes | Base/Logtivity_Api.php +68 -14 3.3.8 → 3.3.10 View file →
@@ -43,8 +43,15 @@
43 43 */
44 44 protected Logtivity_Options $options;
45 45
46 46 /**
47 + * Writes API call data to a file when debug mode is enabled.
48 + *
49 + * @var Logtivity_Debug_Log
50 + */
51 + protected Logtivity_Debug_Log $debugLog;
52 +
53 + /**
47 54 * We generally don't want to wait for a response from the API.
48 55 *
49 56 * @var bool
50 57 */
@@ -66,9 +73,10 @@
66 73 protected ?string $api_key = null;
67 74
68 75 public function __construct()
69 76 {
70 - $this->options = new Logtivity_Options();
77 + $this->options = new Logtivity_Options();
78 + $this->debugLog = new Logtivity_Debug_Log();
71 79 }
72 80
73 81 /**
74 82 * @return $this
@@ -115,8 +123,9 @@
115 123 $body['site_hash'] = $this->options->urlHash();
116 124 }
117 125
118 126 $waitForResponse = $this->waitForResponse || $this->options->shouldLogLatestResponse();
127 + $requestBody = $body;
119 128
120 129 $request = [
121 130 'method' => $method,
122 131 'timeout' => ($waitForResponse ? 6 : 0.01),
@@ -129,14 +138,30 @@
129 138 'body' => $body,
130 139 'cookies' => [],
131 140 ];
132 141
133 - // @TODO: Switch to Logtivity_Response class to get standardized responses
134 - $response = wp_remote_request($this->getEndpoint($url), $request);
142 + /**
143 + * This request must not be cached. It writes a log entry to the API rather
144 + * than reading a cacheable resource, so a cached response would silently
145 + * drop logs. The default path is also non-blocking with a 0.01s timeout,
146 + * so it does not hold up the page load.
147 + *
148 + * Settings check-ins are already rate limited by
149 + * Logtivity_Options::shouldCheckInWithApi(), which gates on the
150 + * logtivity_last_settings_check_in_at option.
151 + *
152 + * @TODO: Switch to Logtivity_Response class to get standardized responses
153 + */
154 + $requestStartedAt = microtime(true);
155 + $response = wp_remote_request($this->getEndpoint($url), $request);
156 + $responseTimeMs = (int)round((microtime(true) - $requestStartedAt) * 1000);
157 +
158 + $responseData = null;
159 +
135 160 if ($waitForResponse && $this->notUpdatingWidgetInCustomizer()) {
136 161 // We waited and received a response
137 162 if ($response instanceof WP_Error) {
138 - $responseData = array_merge(static::$responseData, [
163 + $parsedResponse = array_merge(static::$responseData, [
139 164 'code' => 500,
140 165 'message' => $response->get_error_code(),
141 166 'error' => $response->get_error_message() ?: $response->get_error_code(),
142 167 'body' => get_object_vars($response),
@@ -152,9 +177,9 @@
152 177 } else {
153 178 $responseError = $responseMessage;
154 179 $responseMessage = (($responseBody['message'] ?? $responseMessage) ?: 'Unknown error');
155 180 }
156 - $responseData = [
181 + $parsedResponse = [
157 182 'code' => $responseCode,
158 183 'message' => $responseMessage,
159 184 'error' => $responseError,
160 185 'body' => $responseBody,
@@ -161,12 +186,12 @@
161 186 ];
162 187 }
163 188
164 189 $newStatus = 'success';
165 - if ($responseData['code']) {
166 - if ($responseData['code'] < 400) {
190 + if ($parsedResponse['code']) {
191 + if ($parsedResponse['code'] < 400) {
167 192 // Successful request, check if api is telling us to pause
168 - if ($responseData['error']) {
193 + if ($parsedResponse['error']) {
169 194 $newStatus = 'paused';
170 195 }
171 196
172 197 $body = json_decode($response['body'] ?? 'null', true);
@@ -172,9 +197,9 @@
172 197 $body = json_decode($response['body'] ?? 'null', true);
173 198 $this->updateSettings($body['settings'] ?? []);
174 199
175 200 } else {
176 - if ($this->getCurlError($responseData['error']) != static::CURL_TIMEOUT) {
201 + if ($this->getCurlError($parsedResponse['error']) != static::CURL_TIMEOUT) {
177 202 // Something other than a timeout went wrong with the request
178 203 $newStatus = 'fail';
179 204 }
180 205
@@ -184,17 +209,39 @@
184 209 $this->options->update([
185 210 'logtivity_api_key_check' => $newStatus,
186 211 'logtivity_latest_response' => array_merge(
187 212 ['date' => date('Y-m-d H:i:s')],
188 - $responseData
213 + $parsedResponse
189 214 ),
190 215 ],
191 216 false
192 217 );
193 218
194 - return $responseData;
219 + $responseData = $parsedResponse;
195 220 }
196 221 }
222 +
223 + if ($this->getOption('logtivity_enable_debug_mode')) {
224 + // GET responses (e.g. the paginated /logs listing) can be large
225 + // and aren't useful to store in full; only capture the body for
226 + // the more common POST calls (log/settings sends).
227 + $responseBody = $method === 'GET' ? '(omitted for GET requests)' : ($responseData['body'] ?? null);
228 +
229 + $this->debugLog->write([
230 + 'timestamp' => current_time('mysql'),
231 + 'endpoint' => $url,
232 + 'url' => $this->getEndpoint($url),
233 + 'method' => $method,
234 + 'request_body' => $requestBody,
235 + 'response_code' => $responseData['code'] ?? null,
236 + 'response_message' => $responseData['message'] ?? null,
237 + 'response_error' => $responseData['error'] ?? null,
238 + 'response_body' => $responseData ? $responseBody : null,
239 + 'response_time_ms' => $responseData ? $responseTimeMs : null,
240 + ]);
241 + }
242 +
243 + return $responseData;
197 244 }
198 245
199 246 return null;
200 247 }
@@ -203,9 +250,10 @@
203 250 * @return bool
204 251 */
205 252 protected function ready(): bool
206 253 {
207 - return ($this->getApiKey())
254 + return logtivity_url_environment_mismatch() == false
255 + && ($this->getApiKey())
208 256 && logtivity_has_site_url_changed() == false
209 257 && (
210 258 $this->ignoreStatus
211 259 || $this->getOption('logtivity_api_key_check') == 'success'
@@ -268,10 +316,16 @@
268 316 * @return bool
269 317 */
270 318 protected function notUpdatingWidgetInCustomizer(): bool
271 319 {
272 - $customize = sanitize_text_field($_POST['wp_customize'] ?? null);
273 - $action = sanitize_text_field($_POST['action'] ?? null);
320 + // phpcs:disable WordPress.Security.NonceVerification.Missing -- Passive check avoids option writes during core Customizer widget updates.
321 + $customize = isset($_POST['wp_customize'])
322 + ? sanitize_text_field(wp_unslash($_POST['wp_customize']))
323 + : null;
324 + $action = isset($_POST['action'])
325 + ? sanitize_text_field(wp_unslash($_POST['action']))
326 + : null;
327 + // phpcs:enable WordPress.Security.NonceVerification.Missing
274 328
275 329 return ($action == 'update-widget' && $customize == 'on') == false;
276 330 }
277 331