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

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