PluginProbe
Patchstack – WordPress & Plugins Security / 2.3.7
Patchstack – WordPress & Plugins Security v2.3.7
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
← All changes | includes/api.php +241 -84 2.1.202.3.7 View file →
@@ -15,8 +15,13 @@
15 15 */
16 16 public $blog_id;
17 17
18 18 /**
19 + * @var string Error message from the API.
20 + */
21 + public $message;
22 +
23 + /**
19 24 * Add the actions required for the API.
20 25 *
21 26 * @param Patchstack $core
22 27 * @return void
@@ -23,10 +28,11 @@
23 28 */
24 29 public function __construct( $core ) {
25 30 parent::__construct( $core );
26 31 $this->blog_id = get_current_blog_id();
27 - add_action( 'patchstack_update_license_status', array( $this, 'update_license_status' ) );
28 - add_action( 'patchstack_send_ping', array( $this, 'ping' ) );
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' ] );
29 35 }
30 36
31 37 /**
32 38 * Get the API token.
@@ -50,17 +56,18 @@
50 56 if ( $response && $response->result == 'success' ) {
51 57 $this->update_blog_option(
52 58 $this->blog_id,
53 59 'patchstack_api_token',
54 - array(
60 + [
55 61 'token' => $response->message,
56 62 'expiresin' => $response->expiresin,
57 - )
63 + ]
58 64 );
59 65 return $response->message;
60 66 }
61 67
62 68 // If we reach this, it means we were not able to get the access token.
69 + $this->message = $response;
63 70 $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
64 71 return null;
65 72 }
66 73
@@ -68,24 +75,32 @@
68 75 * Fetch the API Token from API Server.
69 76 *
70 77 * @param string $clientid The API client ID.
71 78 * @param string $secretkey The API secret key.
72 - * @return string|array
79 + * @return string|array|object
73 80 */
74 81 public function fetch_access_token( $clientid = '', $secretkey = '' ) {
75 82 // Skeleton for the response data.
76 - $response_data = (object) array(
83 + $response_data = (object) [
77 84 'result' => '',
78 85 'message' => '',
79 86 'expiresin' => '',
80 - );
87 + ];
81 88
82 89 // Determine if the license id/key is set.
83 - $client_id = $this->get_blog_option( $this->blog_id, 'patchstack_clientid', false ) ? $this->get_blog_option( $this->blog_id, 'patchstack_clientid', false ) : $clientid;
84 - $client_secret = $this->get_blog_option( $this->blog_id, 'patchstack_secretkey', false ) ? $this->get_blog_option( $this->blog_id, 'patchstack_secretkey', false ) : $secretkey;
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.
85 100 if ( empty( $client_id ) || empty( $client_secret ) ) {
86 101 $response_data->result = 'failed';
87 - $response_data->message = __( 'API keys missing! Unable to obtain an access token.', 'patchstack' );
102 + $response_data->message = esc_attr__( 'API keys missing! Unable to obtain an access token.', 'patchstack' );
88 103 return $response_data;
89 104 }
90 105
91 106 // Send a request to our server to obtain the access token.
@@ -90,28 +105,38 @@
90 105
91 106 // Send a request to our server to obtain the access token.
92 107 $response = wp_remote_post(
93 108 $this->plugin->auth_url . '/oauth/token',
94 - array(
109 + [
95 110 'method' => 'POST',
96 111 'timeout' => 60,
97 112 'redirection' => 5,
98 113 'httpversion' => '1.0',
99 114 'blocking' => true,
100 - 'headers' => array(),
101 - 'body' => array(
115 + 'headers' => [],
116 + 'body' => [
102 117 'client_id' => $client_id,
103 118 'client_secret' => $client_secret,
104 119 'grant_type' => 'client_credentials',
105 - ),
106 - 'cookies' => array(),
107 - )
120 + ],
121 + 'cookies' => [],
122 + ]
108 123 );
109 124
110 125 // Stop if we received an error from the API.
111 - if ( is_wp_error( $response ) ) {
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 +
112 136 $response_data->result = 'failed';
113 - $response_data->message = __( 'Unexpected error! Unable to obtain an access token.', 'patchstack' ) . $response->get_error_message();
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;
114 139 return $response_data;
115 140 }
116 141
117 142 // Parse the result.
@@ -124,30 +149,86 @@
124 149 // We need to know when the token expires.
125 150 // Defer to 'expires' if it is provided instead.
126 151 if ( isset( $result->expires_in ) ) {
127 152 if ( ! is_numeric( $result->expires_in ) ) {
153 + $response_data->result = 'failed';
128 154 $response_data->message = 'expires_in value must be an integer';
129 155 return $response_data;
130 156 }
131 157 $response_data->expiresin = $result->expires_in != 0 ? time() + $result->expires_in : 0;
132 - } elseif ( ! empty( $result->expires_in ) ) {
133 - // Some providers supply the seconds until expiration rather than
134 - // the exact timestamp. Take a best guess at which we received.
135 - $expires = $options['expires'];
136 - if ( ! $this->isExpirationTimestamp( $expires ) ) {
137 - $expires += time();
138 - }
139 - $response_data->expiresin = $expires;
140 158 }
159 +
141 160 return $response_data;
142 161 } elseif ( isset( $result->error ) ) {
143 162 $response_data->result = $result->error;
144 - $response_data->message = __( 'Unexpected error! Unable to obtain an access token.', 'patchstack' ) . $result->message;
163 + $response_data->message = esc_attr__( 'Unexpected error! Unable to obtain an access token.', 'patchstack' ) . $result->message;
145 164 return $response_data;
146 165 }
147 166 }
148 167
149 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 + /**
150 231 * Checks if the API token has expired.
151 232 *
152 233 * @param integer $expiresin API token expiry.
153 234 * @return boolean If the token has expired.
@@ -152,8 +233,12 @@
152 233 * @param integer $expiresin API token expiry.
153 234 * @return boolean If the token has expired.
154 235 */
155 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 + }
156 241 return ( $expiresin < ( time() + 30 ) );
157 242 }
158 243
159 244 /**
@@ -158,75 +243,121 @@
158 243
159 244 /**
160 245 * Retrieve the status of a license.
161 246 *
247 + * @param boolean $fetchPolicy Whether or not to fetch the policy settings.
162 248 * @return void|array
163 249 */
164 - public function update_license_status() {
250 + public function update_license_status($fetchPolicy = false) {
165 251 // Get current license status.
166 - $response = $this->send_request( '/api/license/verify', 'GET' );
252 + $response = $this->send_request( '/api/license/verify' . ($fetchPolicy ? '?fetchPolicy=true' : ''), 'GET' );
167 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 +
168 263 // Update the representing options.
264 + // Expiry date.
169 265 if ( isset( $response['expires_at'] ) ) {
170 266 $this->update_blog_option( $this->blog_id, 'patchstack_license_expiry', $response['expires_at'] );
171 267 }
172 268
269 + // Free vs Paid license.
173 270 if ( isset( $response['free'] ) ) {
174 271 $this->update_blog_option( $this->blog_id, 'patchstack_license_free', $response['free'] == false ? 0 : 1 );
175 272
176 273 if ( $response['free'] == true ) {
177 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();
178 278 }
179 279 }
180 280
181 - if ( isset( $response['active'] ) && $response['active'] == true ) {
182 - $this->update_blog_option( $this->blog_id, 'patchstack_license_activated', true );
281 + // Active subscription.
282 + if ( isset( $response['active'] ) ) {
283 + $this->update_blog_option( $this->blog_id, 'patchstack_license_activated', $response['active'] == true ? 1 : 0 );
183 284 }
184 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 +
185 321 return $response;
186 322 }
187 323
188 324 /**
189 - * Send a request to the API with optionally POST data.
190 - *
191 - * @param string $url
192 - * @param string $request
193 - * @param array $data
194 - * @return void|array If successful array, otherwise void.
325 + * Send a request to our API for the IP address header.
326 + *
327 + * @return void
195 328 */
196 - public function send_request( $url, $request, $data = array() ) {
197 - // Attempt to get the access token.
198 - $token = $this->get_access_token();
199 - if ( empty( $token ) ) {
200 - return;
201 - }
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 );
202 334
203 - // Send the remote request using the WordPress built-in method.
204 - $response = wp_remote_request(
205 - $this->plugin->api_url . $url,
206 - array(
207 - 'method' => $request,
208 - 'timeout' => 60,
209 - 'redirection' => 5,
210 - 'httpversion' => '1.0',
211 - 'blocking' => true,
212 - 'headers' => array(
213 - 'Authorization' => 'Bearer ' . $token,
214 - 'LicenseID' => $this->get_blog_option( $this->blog_id, 'patchstack_clientid', 0 ),
215 - 'Source-Host' => get_site_url(),
216 - ),
217 - 'body' => $data,
218 - 'cookies' => array(),
219 - )
220 - );
221 -
222 - // Check error or status code.
223 - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) != 200 ) {
224 - $this->update_blog_option( $this->blog_id, 'patchstack_api_token', '' );
225 - return;
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 + );
226 359 }
227 -
228 - return json_decode( wp_remote_retrieve_body( $response ), true );
229 360 }
230 361
231 362 /**
232 363 * Get the firewall rules.
@@ -233,14 +364,9 @@
233 364 *
234 365 * @return array The firewall rules.
235 366 */
236 367 public function post_firewall_rule_json() {
237 - // If the request is coming from the API, fetch fresh rules.
238 - if ( isset( $_POST['webarx_refresh_rules'] ) ) {
239 - return $this->send_request( '/api/get-rules/2?bypass=cache', 'POST' );
240 - }
241 -
242 - return $this->send_request( '/api/get-rules/2', 'POST' );
368 + return $this->send_request( '/api/get-rules/3', 'POST' );
243 369 }
244 370
245 371 /**
246 372 * Get the .htaccess rules.
@@ -252,17 +378,8 @@
252 378 return $this->send_request( '/api/rules', 'POST', $settings );
253 379 }
254 380
255 381 /**
256 - * Get the .htaccess firewall rules.
257 - *
258 - * @return array The .htaccess rules.
259 - */
260 - public function post_firewall_htaccess_rule() {
261 - return $this->send_request( '/api/rules/htaccess', 'POST' );
262 - }
263 -
264 - /**
265 382 * Send the firewall logs to the API.
266 383 *
267 384 * @param array $logs
268 385 * @return array
@@ -330,7 +447,47 @@
330 447 *
331 448 * @return void
332 449 */
333 450 public function ping() {
334 - $this->send_request( '/api/ping', 'POST', array( 'firewall' => $this->get_option( 'patchstack_basic_firewall' ) == 1 ? 1 : 0 ) );
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;
335 492 }
336 493 }