PluginProbe
Patchstack – WordPress & Plugins Security / trunk
Patchstack – WordPress & Plugins Security vtrunk
2.3.7 trunk 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.17 2.1.18 2.1.19 2.1.2 2.1.20 2.1.21 2.1.22 2.1.23 2.1.24 2.1.25 2.1.3 2.1.4 2.1.5 2.1.6 All 49 releases
patchstack / includes / api.php

api.php in Patchstack – WordPress & Plugins Security trunk, at includes/api.php

494 lines 15.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // Do not allow the file to be called directly.
4 if ( ! defined( 'ABSPATH' ) ) {
5 exit;
6 }
7
8 /**
9 * This class is used as a base for communicating with the Patchstack API.
10 */
11 class P_Api extends P_Core {
12
13 /**
14 * @var integer The current blog id.
15 */
16 public $blog_id;
17
18 /**
19 * @var string Error message from the API.
20 */
21 public $message;
22
23 /**
24 * Add the actions required for the API.
25 *
26 * @param Patchstack $core
27 * @return void
28 */
29 public function __construct( $core ) {
30 parent::__construct( $core );
31 $this->blog_id = get_current_blog_id();
32 add_action( 'patchstack_update_license_status', [ $this, 'update_license_status' ] );
33 add_action( 'patchstack_send_ping', [ $this, 'ping' ] );
34 add_action( 'patchstack_send_header_request', [ $this, 'send_header_request' ] );
35 }
36
37 /**
38 * Get the API token.
39 *
40 * @param string $clientid The API client ID.
41 * @param string $secretkey The API secret key.
42 * @param boolean $fresh Whether or not to get a fresh token.
43 * @return null|string
44 */
45 public function get_access_token( $clientid = '', $secretkey = '', $fresh = false ) {
46 // Get current access token, if it exists.
47 $token_data = $this->get_blog_option( $this->blog_id, 'patchstack_api_token', false );
48
49 // If we do not need a fresh token, get the current one if it's not expired.
50 if ( ! $fresh && isset( $token_data['token'] ) && ! $this->has_expired( $token_data['expiresin'] ) ) {
51 return $token_data['token'];
52 }
53
54 // Call API and get the new access token.
55 $response = $this->fetch_access_token( $clientid, $secretkey );
56 if ( $response && $response->result == 'success' ) {
57 $this->update_blog_option(
58 $this->blog_id,
59 'patchstack_api_token',
60 [
61 'token' => $response->message,
62 'expiresin' => $response->expiresin,
63 ]
64 );
65 return $response->message;
66 }
67
68 // If we reach this, it means we were not able to get the access token.
69 $this->message = $response;
70 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
71 return null;
72 }
73
74 /**
75 * Fetch the API Token from API Server.
76 *
77 * @param string $clientid The API client ID.
78 * @param string $secretkey The API secret key.
79 * @return string|array|object
80 */
81 public function fetch_access_token( $clientid = '', $secretkey = '' ) {
82 // Skeleton for the response data.
83 $response_data = (object) [
84 'result' => '',
85 'message' => '',
86 'expiresin' => '',
87 ];
88
89 // Determine if the license id/key is set.
90 $client_id = $this->get_blog_option( $this->blog_id, 'patchstack_clientid', $clientid );
91
92 // Decrypt the secret key, if it is encrypted.
93 $client_secret = $this->get_blog_option( $this->blog_id, 'patchstack_secretkey', $secretkey );
94 $client_nonce = $this->get_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', false );
95 if ( $client_nonce ) {
96 $client_secret = $this->decrypt( $client_secret, $client_nonce );
97 }
98
99 // Make sure these values are set.
100 if ( empty( $client_id ) || empty( $client_secret ) ) {
101 $response_data->result = 'failed';
102 $response_data->message = esc_attr__( 'API keys missing! Unable to obtain an access token.', 'patchstack' );
103 return $response_data;
104 }
105
106 // Send a request to our server to obtain the access token.
107 $response = wp_remote_post(
108 $this->plugin->auth_url . '/oauth/token',
109 [
110 'method' => 'POST',
111 'timeout' => 60,
112 'redirection' => 5,
113 'httpversion' => '1.0',
114 'blocking' => true,
115 'headers' => [],
116 'body' => [
117 'client_id' => $client_id,
118 'client_secret' => $client_secret,
119 'grant_type' => 'client_credentials',
120 ],
121 'cookies' => [],
122 ]
123 );
124
125 // Stop if we received an error from the API.
126 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) == 401 ) {
127 $this->message = wp_remote_retrieve_body( $response );
128
129 if ( wp_remote_retrieve_response_code( $response ) == 401 ) {
130 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
131 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
132 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
133 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
134 }
135
136 $response_data->result = 'failed';
137 $response_data->message = esc_attr__( 'Unexpected error! Unable to obtain an access token. Error code: ', 'patchstack' ) . wp_remote_retrieve_response_code( $response );
138 $response_data->body = $this->message;
139 return $response_data;
140 }
141
142 // Parse the result.
143 $result = json_decode( wp_remote_retrieve_body( $response ) );
144 if ( isset( $result->access_token ) ) {
145 $response_data->result = 'success';
146 $response_data->message = $result->access_token;
147 $response_data->expiresin = $result->expires_in;
148
149 // We need to know when the token expires.
150 // Defer to 'expires' if it is provided instead.
151 if ( isset( $result->expires_in ) ) {
152 if ( ! is_numeric( $result->expires_in ) ) {
153 $response_data->result = 'failed';
154 $response_data->message = 'expires_in value must be an integer';
155 return $response_data;
156 }
157 $response_data->expiresin = $result->expires_in != 0 ? time() + $result->expires_in : 0;
158 }
159
160 return $response_data;
161 } elseif ( isset( $result->error ) ) {
162 $response_data->result = $result->error;
163 $response_data->message = esc_attr__( 'Unexpected error! Unable to obtain an access token.', 'patchstack' ) . $result->message;
164 return $response_data;
165 }
166 }
167
168 /**
169 * Send a request to the API with optionally POST data.
170 *
171 * @param string $url
172 * @param string $method
173 * @param array $data
174 * @return void|array If successful array, otherwise void.
175 */
176 public function send_request( $url, $method, $data = [] ) {
177 // Attempt to get the access token.
178 $token = $this->get_access_token();
179 if ( empty( $token ) ) {
180 return;
181 }
182
183 // Pass the multisite value to all requests, only for POST requests.
184 if ( $method == 'POST' ) {
185 $data['is_multisite'] = $this->is_multi_site ? 1 : 0;
186 }
187
188 // Send the remote request using the WordPress built-in method.
189 $response = wp_remote_request(
190 $this->plugin->api_url . $url,
191 [
192 'method' => $method,
193 'timeout' => 60,
194 'redirection' => 5,
195 'httpversion' => '1.0',
196 'blocking' => true,
197 'headers' => [
198 'Authorization' => 'Bearer ' . $token,
199 'LicenseID' => $this->get_blog_option( $this->blog_id, 'patchstack_clientid', 0 ),
200 'Source-Host' => get_site_url(),
201 ],
202 'body' => $data,
203 'cookies' => [],
204 ]
205 );
206
207 // Check error or status code.
208 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
209
210 // See if we received a site API connection termination.
211 $body = json_decode( wp_remote_retrieve_body( $response ), true );
212 if ( isset( $body['cancel'] ) ) {
213 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
214 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
215 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
216 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
217 }
218
219 return wp_remote_retrieve_response_code( $response );
220 }
221
222 // A 200 OK means we successfully communicated with the API for this sync
223 // action (license verify, log/software upload, rule pull, ping, etc.), so
224 // record it as the last successful sync time.
225 $this->update_blog_option( $this->blog_id, 'patchstack_last_sync', time() );
226
227 return json_decode( wp_remote_retrieve_body( $response ), true );
228 }
229
230 /**
231 * Checks if the API token has expired.
232 *
233 * @param integer $expiresin API token expiry.
234 * @return boolean If the token has expired.
235 */
236 public function has_expired( $expiresin ) {
237 // A stored expiry of 0 means the token never expires.
238 if ( $expiresin === 0 ) {
239 return false;
240 }
241 return ( $expiresin < ( time() + 30 ) );
242 }
243
244 /**
245 * Retrieve the status of a license.
246 *
247 * @param boolean $fetchPolicy Whether or not to fetch the policy settings.
248 * @return void|array
249 */
250 public function update_license_status($fetchPolicy = false) {
251 // Get current license status.
252 $response = $this->send_request( '/api/license/verify' . ($fetchPolicy ? '?fetchPolicy=true' : ''), 'GET' );
253
254 // Invalid license, or no longer active.
255 if ( ! is_array( $response ) && $response == 422 ) {
256 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
257 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
258 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
259 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
260 return;
261 }
262
263 // Update the representing options.
264 // Expiry date.
265 if ( isset( $response['expires_at'] ) ) {
266 $this->update_blog_option( $this->blog_id, 'patchstack_license_expiry', $response['expires_at'] );
267 }
268
269 // Free vs Paid license.
270 if ( isset( $response['free'] ) ) {
271 $this->update_blog_option( $this->blog_id, 'patchstack_license_free', $response['free'] == false ? 0 : 1 );
272
273 if ( $response['free'] == true ) {
274 $this->update_blog_option( $this->blog_id, 'patchstack_show_settings', 0 );
275 $this->update_blog_option( $this->blog_id, 'patchstack_firewall_rules_v3', '[]' );
276 } else {
277 $this->send_header_request();
278 }
279 }
280
281 // Active subscription.
282 if ( isset( $response['active'] ) ) {
283 $this->update_blog_option( $this->blog_id, 'patchstack_license_activated', $response['active'] == true ? 1 : 0 );
284 }
285
286 // Subscription class.
287 if ( isset( $response['class'] ) ) {
288 $this->update_blog_option( $this->blog_id, 'patchstack_subscription_class', $response['class'] );
289 $this->update_blog_option( $this->blog_id, 'patchstack_last_license_check', time() );
290 }
291
292 // Managed site status.
293 if ( isset( $response['managed'], $response['managed_string'] ) ) {
294 $this->update_blog_option( $this->blog_id, 'patchstack_managed', $response['managed'] ? 1 : 0 );
295 $this->update_blog_option( $this->blog_id, 'patchstack_managed_text', $response['managed_string'] );
296 }
297
298 // Site ID.
299 if ( isset( $response['site_id'] ) ) {
300 $this->update_blog_option( $this->blog_id, 'patchstack_site_id', $response['site_id'] );
301 }
302
303 // Policy settings.
304 if ( isset( $response['policy'] ) && is_array( $response['policy'] ) && count( $response['policy'] ) > 0 ) {
305 foreach ( $response['policy'] as $key => $value ) {
306 // Make sure the option exists.
307 if ( ! array_key_exists( $key, $this->plugin->admin_options->options ) ) {
308 continue;
309 }
310
311 // Booleans would persist as '1' / '' otherwise, store them as 1/0 so type checks behave consistently.
312 if ( is_bool( $value ) ) {
313 $value = $value ? 1 : 0;
314 }
315
316 // Update the option.
317 $this->update_blog_option( $this->blog_id, $key, $value );
318 }
319 }
320
321 return $response;
322 }
323
324 /**
325 * Send a request to our API for the IP address header.
326 *
327 * @return void
328 */
329 public function send_header_request()
330 {
331 $header = get_option( 'patchstack_firewall_ip_header', '' );
332 $computed = get_option( 'patchstack_ip_header_computed', 0 );
333 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
334
335 if ( ( $header == '' && ! $computed ) || $force ) {
336 // Create an OTT token.
337 $ott = md5( wp_generate_password( 32, true, true ) );
338 update_option( 'patchstack_ott_action', $ott );
339
340 // Tell our API.
341 wp_remote_request(
342 $this->plugin->api_url . '/api/header',
343 [
344 'method' => 'POST',
345 'timeout' => 60,
346 'redirection' => 5,
347 'httpversion' => '1.0',
348 'blocking' => true,
349 'headers' => [
350 'Source-Host' => get_site_url(),
351 ],
352 'body' => [
353 'token' => $ott,
354 'url' => get_site_url()
355 ],
356 'cookies' => [],
357 ]
358 );
359 }
360 }
361
362 /**
363 * Get the firewall rules.
364 *
365 * @return array The firewall rules.
366 */
367 public function post_firewall_rule_json() {
368 return $this->send_request( '/api/get-rules/3', 'POST' );
369 }
370
371 /**
372 * Get the .htaccess rules.
373 *
374 * @param array $settings The settings on which .htaccess rules to get.
375 * @return array The .htaccess rules.
376 */
377 public function post_firewall_rule( $settings ) {
378 return $this->send_request( '/api/rules', 'POST', $settings );
379 }
380
381 /**
382 * Send the firewall logs to the API.
383 *
384 * @param array $logs
385 * @return array
386 */
387 public function upload_firewall_logs( $logs ) {
388 return $this->send_request( '/api/logs/log', 'POST', $logs );
389 }
390
391 /**
392 * Send the activity logs to the server.
393 *
394 * @param array $logs
395 * @return array
396 */
397 public function upload_activity_logs( $logs ) {
398 return $this->send_request( '/api/activity/log', 'POST', $logs );
399 }
400
401 /**
402 * Send WordPress core, theme, plugins versions and information to the API.
403 *
404 * @param array $software
405 * @return array
406 */
407 public function upload_software( $software ) {
408 return $this->send_request( '/api/sw/json', 'POST', $software );
409 }
410
411 /**
412 * Update the firewall status.
413 *
414 * @param array $status
415 * @return array
416 */
417 public function update_firewall_status( $status ) {
418 if ( $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
419 return;
420 }
421
422 return $this->send_request( '/api/firewall/update/status', 'POST', $status );
423 }
424
425 /**
426 * Update the URL on the API.
427 *
428 * @param array $url The current URL of the site.
429 * @return array
430 */
431 public function update_url( $url ) {
432 return $this->send_request( '/api/plugin/update/url', 'POST', $url );
433 }
434
435 /**
436 * Send list of sites and get the id and secret key in response.
437 *
438 * @param array $sites
439 * @return array
440 */
441 public function get_site_licenses( $sites ) {
442 return $this->send_request( '/api/multisite-keys', 'POST', $sites );
443 }
444
445 /**
446 * Send a ping to the Patchstack API every 3 hours to make sure that the plugin is still running.
447 *
448 * @return void
449 */
450 public function ping() {
451 $this->send_request( '/api/ping', 'POST', [ 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ? 1 : 0 ] );
452 }
453
454 /**
455 * Generate a secret value and send it to the Patchstack API for quick activation.
456 *
457 * @param string $secret
458 * @return void
459 */
460 public function send_secret_token( $secret ) {
461 $response = wp_remote_request(
462 $this->plugin->api_url . '/api/secret',
463 [
464 'method' => 'POST',
465 'timeout' => 60,
466 'redirection' => 5,
467 'httpversion' => '1.0',
468 'blocking' => true,
469 'headers' => [
470 'Source-Host' => get_site_url(),
471 ],
472 'body' => [
473 'secret' => $secret,
474 'url' => get_site_url()
475 ],
476 'cookies' => [],
477 ]
478 );
479
480 // Check error or status code.
481 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
482 return false;
483 }
484
485 // Determine if auto-activation succeeded.
486 $result = json_decode( wp_remote_retrieve_body( $response ), true );
487 if ($result && isset($result['activated'])) {
488 return $result['activated'];
489 }
490
491 return false;
492 }
493 }
494