PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.5
Patchstack – WordPress & Plugins Security v2.3.5
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 2.3.5, at includes/api.php

479 lines 14.6 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->message = 'expires_in value must be an integer';
154 return $response_data;
155 }
156 $response_data->expiresin = $result->expires_in != 0 ? time() + $result->expires_in : 0;
157 }
158
159 return $response_data;
160 } elseif ( isset( $result->error ) ) {
161 $response_data->result = $result->error;
162 $response_data->message = esc_attr__( 'Unexpected error! Unable to obtain an access token.', 'patchstack' ) . $result->message;
163 return $response_data;
164 }
165 }
166
167 /**
168 * Send a request to the API with optionally POST data.
169 *
170 * @param string $url
171 * @param string $method
172 * @param array $data
173 * @return void|array If successful array, otherwise void.
174 */
175 public function send_request( $url, $method, $data = [] ) {
176 // Attempt to get the access token.
177 $token = $this->get_access_token();
178 if ( empty( $token ) ) {
179 return;
180 }
181
182 // Pass the multisite value to all requests, only for POST requests.
183 if ( $method == 'POST' ) {
184 $data['is_multisite'] = $this->is_multi_site ? 1 : 0;
185 }
186
187 // Send the remote request using the WordPress built-in method.
188 $response = wp_remote_request(
189 $this->plugin->api_url . $url,
190 [
191 'method' => $method,
192 'timeout' => 60,
193 'redirection' => 5,
194 'httpversion' => '1.0',
195 'blocking' => true,
196 'headers' => [
197 'Authorization' => 'Bearer ' . $token,
198 'LicenseID' => $this->get_blog_option( $this->blog_id, 'patchstack_clientid', 0 ),
199 'Source-Host' => get_site_url(),
200 ],
201 'body' => $data,
202 'cookies' => [],
203 ]
204 );
205
206 // Check error or status code.
207 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
208
209 // See if we received a site API connection termination.
210 $body = json_decode( wp_remote_retrieve_body( $response ), true );
211 if ( isset( $body['cancel'] ) ) {
212 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
213 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
214 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
215 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
216 }
217
218 return wp_remote_retrieve_response_code( $response );
219 }
220
221 return json_decode( wp_remote_retrieve_body( $response ), true );
222 }
223
224 /**
225 * Checks if the API token has expired.
226 *
227 * @param integer $expiresin API token expiry.
228 * @return boolean If the token has expired.
229 */
230 public function has_expired( $expiresin ) {
231 return ( $expiresin < ( time() + 30 ) );
232 }
233
234 /**
235 * Retrieve the status of a license.
236 *
237 * @param boolean $fetchPolicy Whether or not to fetch the policy settings.
238 * @return void|array
239 */
240 public function update_license_status($fetchPolicy = false) {
241 // Get current license status.
242 $response = $this->send_request( '/api/license/verify' . ($fetchPolicy ? '?fetchPolicy=true' : ''), 'GET' );
243
244 // Invalid license, or no longer active.
245 if ( ! is_array( $response ) && $response == 422 ) {
246 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
247 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
248 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
249 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
250 return;
251 }
252
253 // Update the representing options.
254 // Expiry date.
255 if ( isset( $response['expires_at'] ) ) {
256 $this->update_blog_option( $this->blog_id, 'patchstack_license_expiry', $response['expires_at'] );
257 }
258
259 // Free vs Paid license.
260 if ( isset( $response['free'] ) ) {
261 $this->update_blog_option( $this->blog_id, 'patchstack_license_free', $response['free'] == false ? 0 : 1 );
262
263 if ( $response['free'] == true ) {
264 $this->update_blog_option( $this->blog_id, 'patchstack_show_settings', 0 );
265 $this->update_blog_option( $this->blog_id, 'patchstack_firewall_rules_v3', '[]' );
266 } else {
267 $this->send_header_request();
268 }
269 }
270
271 // Active subscription.
272 if ( isset( $response['active'] ) ) {
273 $this->update_blog_option( $this->blog_id, 'patchstack_license_activated', $response['active'] == true );
274 }
275
276 // Subscription class.
277 if ( isset( $response['class'] ) ) {
278 $this->update_blog_option( $this->blog_id, 'patchstack_subscription_class', $response['class'] );
279 $this->update_blog_option( $this->blog_id, 'patchstack_last_license_check', time() );
280 }
281
282 // Managed site status.
283 if ( isset( $response['managed'], $response['managed_string'] ) ) {
284 $this->update_blog_option( $this->blog_id, 'patchstack_managed', $response['managed'] );
285 $this->update_blog_option( $this->blog_id, 'patchstack_managed_text', $response['managed_string'] );
286 }
287
288 // Site ID.
289 if ( isset( $response['site_id'] ) ) {
290 $this->update_blog_option( $this->blog_id, 'patchstack_site_id', $response['site_id'] );
291 }
292
293 // Policy settings.
294 if ( isset( $response['policy'] ) && is_array( $response['policy'] ) && count( $response['policy'] ) > 0 ) {
295 foreach ( $response['policy'] as $key => $value ) {
296 // Make sure the option exists.
297 if ( ! array_key_exists( $key, $this->plugin->admin_options->options ) ) {
298 continue;
299 }
300
301 // Update the option.
302 $this->update_blog_option( $this->blog_id, $key, $value );
303 }
304 }
305
306 return $response;
307 }
308
309 /**
310 * Send a request to our API for the IP address header.
311 *
312 * @return void
313 */
314 public function send_header_request()
315 {
316 $header = get_option( 'patchstack_firewall_ip_header', '' );
317 $computed = get_option( 'patchstack_ip_header_computed', 0 );
318 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
319
320 if ( ( $header == '' && ! $computed ) || $force ) {
321 // Create an OTT token.
322 $ott = md5( wp_generate_password( 32, true, true ) );
323 update_option( 'patchstack_ott_action', $ott );
324
325 // Tell our API.
326 wp_remote_request(
327 $this->plugin->api_url . '/api/header',
328 [
329 'method' => 'POST',
330 'timeout' => 60,
331 'redirection' => 5,
332 'httpversion' => '1.0',
333 'blocking' => true,
334 'headers' => [
335 'Source-Host' => get_site_url(),
336 ],
337 'body' => [
338 'token' => $ott,
339 'url' => get_site_url()
340 ],
341 'cookies' => [],
342 ]
343 );
344 }
345 }
346
347 /**
348 * Get the firewall rules.
349 *
350 * @return array The firewall rules.
351 */
352 public function post_firewall_rule_json() {
353 return $this->send_request( '/api/get-rules/3', 'POST' );
354 }
355
356 /**
357 * Get the .htaccess rules.
358 *
359 * @param array $settings The settings on which .htaccess rules to get.
360 * @return array The .htaccess rules.
361 */
362 public function post_firewall_rule( $settings ) {
363 return $this->send_request( '/api/rules', 'POST', $settings );
364 }
365
366 /**
367 * Send the firewall logs to the API.
368 *
369 * @param array $logs
370 * @return array
371 */
372 public function upload_firewall_logs( $logs ) {
373 return $this->send_request( '/api/logs/log', 'POST', $logs );
374 }
375
376 /**
377 * Send the activity logs to the server.
378 *
379 * @param array $logs
380 * @return array
381 */
382 public function upload_activity_logs( $logs ) {
383 return $this->send_request( '/api/activity/log', 'POST', $logs );
384 }
385
386 /**
387 * Send WordPress core, theme, plugins versions and information to the API.
388 *
389 * @param array $software
390 * @return array
391 */
392 public function upload_software( $software ) {
393 return $this->send_request( '/api/sw/json', 'POST', $software );
394 }
395
396 /**
397 * Update the firewall status.
398 *
399 * @param array $status
400 * @return array
401 */
402 public function update_firewall_status( $status ) {
403 if ( $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
404 return;
405 }
406
407 return $this->send_request( '/api/firewall/update/status', 'POST', $status );
408 }
409
410 /**
411 * Update the URL on the API.
412 *
413 * @param array $url The current URL of the site.
414 * @return array
415 */
416 public function update_url( $url ) {
417 return $this->send_request( '/api/plugin/update/url', 'POST', $url );
418 }
419
420 /**
421 * Send list of sites and get the id and secret key in response.
422 *
423 * @param array $sites
424 * @return array
425 */
426 public function get_site_licenses( $sites ) {
427 return $this->send_request( '/api/multisite-keys', 'POST', $sites );
428 }
429
430 /**
431 * Send a ping to the Patchstack API every 3 hours to make sure that the plugin is still running.
432 *
433 * @return void
434 */
435 public function ping() {
436 $this->send_request( '/api/ping', 'POST', [ 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ? 1 : 0 ] );
437 }
438
439 /**
440 * Generate a secret value and send it to the Patchstack API for quick activation.
441 *
442 * @param string $secret
443 * @return void
444 */
445 public function send_secret_token( $secret ) {
446 $response = wp_remote_request(
447 $this->plugin->api_url . '/api/secret',
448 [
449 'method' => 'POST',
450 'timeout' => 60,
451 'redirection' => 5,
452 'httpversion' => '1.0',
453 'blocking' => true,
454 'headers' => [
455 'Source-Host' => get_site_url(),
456 ],
457 'body' => [
458 'secret' => $secret,
459 'url' => get_site_url()
460 ],
461 'cookies' => [],
462 ]
463 );
464
465 // Check error or status code.
466 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
467 return false;
468 }
469
470 // Determine if auto-activation succeeded.
471 $result = json_decode( wp_remote_retrieve_body( $response ), true );
472 if ($result && isset($result['activated'])) {
473 return $result['activated'];
474 }
475
476 return false;
477 }
478 }
479