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

468 lines 14.1 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 * @return void|array
238 */
239 public function update_license_status() {
240 // Get current license status.
241 $response = $this->send_request( '/api/license/verify', 'GET' );
242
243 // Invalid license, or no longer active.
244 if ( ! is_array( $response ) && $response == 422 ) {
245 $this->update_blog_option( $this->blog_id, 'patchstack_clientid', '' );
246 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey', '' );
247 $this->update_blog_option( $this->blog_id, 'patchstack_secretkey_nonce', '' );
248 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
249 return;
250 }
251
252 // Update the representing options.
253 if ( isset( $response['expires_at'] ) ) {
254 $this->update_blog_option( $this->blog_id, 'patchstack_license_expiry', $response['expires_at'] );
255 }
256
257 if ( isset( $response['free'] ) ) {
258 $this->update_blog_option( $this->blog_id, 'patchstack_license_free', $response['free'] == false ? 0 : 1 );
259
260 if ( $response['free'] == true ) {
261 $this->update_blog_option( $this->blog_id, 'patchstack_show_settings', 0 );
262 $this->update_blog_option( $this->blog_id, 'patchstack_firewall_rules_v3', '[]' );
263 } else {
264 $this->send_header_request();
265 }
266 }
267
268 if ( isset( $response['active'] ) ) {
269 $this->update_blog_option( $this->blog_id, 'patchstack_license_activated', $response['active'] == true );
270 }
271
272 if ( isset( $response['class'] ) ) {
273 $this->update_blog_option( $this->blog_id, 'patchstack_subscription_class', $response['class'] );
274 $this->update_blog_option( $this->blog_id, 'patchstack_last_license_check', time() );
275 }
276
277 if ( isset( $response['managed'], $response['managed_string'] ) ) {
278 $this->update_blog_option( $this->blog_id, 'patchstack_managed', $response['managed'] );
279 $this->update_blog_option( $this->blog_id, 'patchstack_managed_text', $response['managed_string'] );
280 }
281
282 if ( isset( $response['site_id'] ) ) {
283 $this->update_blog_option( $this->blog_id, 'patchstack_site_id', $response['site_id'] );
284 }
285
286 return $response;
287 }
288
289 /**
290 * Send a request to our API for the IP address header.
291 *
292 * @return void
293 */
294 public function send_header_request()
295 {
296 $header = get_option( 'patchstack_firewall_ip_header', '' );
297 $computed = get_option( 'patchstack_ip_header_computed', 0 );
298 $force = get_option( 'patchstack_ip_header_force_compute', 0 );
299
300 if ( ( $header == '' && ! $computed ) || $force ) {
301 // Create an OTT token.
302 $ott = md5( wp_generate_password( 32, true, true ) );
303 update_option( 'patchstack_ott_action', $ott );
304
305 // Tell our API.
306 wp_remote_request(
307 $this->plugin->api_url . '/api/header',
308 [
309 'method' => 'POST',
310 'timeout' => 60,
311 'redirection' => 5,
312 'httpversion' => '1.0',
313 'blocking' => true,
314 'headers' => [
315 'Source-Host' => get_site_url(),
316 ],
317 'body' => [
318 'token' => $ott,
319 'url' => get_site_url()
320 ],
321 'cookies' => [],
322 ]
323 );
324 }
325 }
326
327 /**
328 * Get the firewall rules.
329 *
330 * @return array The firewall rules.
331 */
332 public function post_firewall_rule_json() {
333 return $this->send_request( '/api/get-rules/3', 'POST' );
334 }
335
336 /**
337 * Get the .htaccess rules.
338 *
339 * @param array $settings The settings on which .htaccess rules to get.
340 * @return array The .htaccess rules.
341 */
342 public function post_firewall_rule( $settings ) {
343 return $this->send_request( '/api/rules', 'POST', $settings );
344 }
345
346 /**
347 * Get the .htaccess firewall rules.
348 *
349 * @return array The .htaccess rules.
350 */
351 public function post_firewall_htaccess_rule() {
352 return $this->send_request( '/api/rules/htaccess', 'POST' );
353 }
354
355 /**
356 * Send the firewall logs to the API.
357 *
358 * @param array $logs
359 * @return array
360 */
361 public function upload_firewall_logs( $logs ) {
362 return $this->send_request( '/api/logs/log', 'POST', $logs );
363 }
364
365 /**
366 * Send the activity logs to the server.
367 *
368 * @param array $logs
369 * @return array
370 */
371 public function upload_activity_logs( $logs ) {
372 return $this->send_request( '/api/activity/log', 'POST', $logs );
373 }
374
375 /**
376 * Send WordPress core, theme, plugins versions and information to the API.
377 *
378 * @param array $software
379 * @return array
380 */
381 public function upload_software( $software ) {
382 return $this->send_request( '/api/sw/json', 'POST', $software );
383 }
384
385 /**
386 * Update the firewall status.
387 *
388 * @param array $status
389 * @return array
390 */
391 public function update_firewall_status( $status ) {
392 if ( $this->get_option( 'patchstack_license_free', 0 ) == 1 ) {
393 return;
394 }
395
396 return $this->send_request( '/api/firewall/update/status', 'POST', $status );
397 }
398
399 /**
400 * Update the URL on the API.
401 *
402 * @param array $url The current URL of the site.
403 * @return array
404 */
405 public function update_url( $url ) {
406 return $this->send_request( '/api/plugin/update/url', 'POST', $url );
407 }
408
409 /**
410 * Send list of sites and get the id and secret key in response.
411 *
412 * @param array $sites
413 * @return array
414 */
415 public function get_site_licenses( $sites ) {
416 return $this->send_request( '/api/multisite-keys', 'POST', $sites );
417 }
418
419 /**
420 * Send a ping to the Patchstack API every 3 hours to make sure that the plugin is still running.
421 *
422 * @return void
423 */
424 public function ping() {
425 $this->send_request( '/api/ping', 'POST', [ 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ? 1 : 0 ] );
426 }
427
428 /**
429 * Generate a secret value and send it to the Patchstack API for quick activation.
430 *
431 * @param string $secret
432 * @return void
433 */
434 public function send_secret_token( $secret ) {
435 $response = wp_remote_request(
436 $this->plugin->api_url . '/api/secret',
437 [
438 'method' => 'POST',
439 'timeout' => 60,
440 'redirection' => 5,
441 'httpversion' => '1.0',
442 'blocking' => true,
443 'headers' => [
444 'Source-Host' => get_site_url(),
445 ],
446 'body' => [
447 'secret' => $secret,
448 'url' => get_site_url()
449 ],
450 'cookies' => [],
451 ]
452 );
453
454 // Check error or status code.
455 if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
456 return false;
457 }
458
459 // Determine if auto-activation succeeded.
460 $result = json_decode( wp_remote_retrieve_body( $response ), true );
461 if ($result && isset($result['activated'])) {
462 return $result['activated'];
463 }
464
465 return false;
466 }
467 }
468