PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.3.1
Defender Security – Malware Scanner, Login Security & Firewall v5.3.1
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / extra / hub-connector / inc / class-api.php

class-api.php in Defender Security – Malware Scanner, Login Security & Firewall 5.3.1, at extra/hub-connector/inc/class-api.php

585 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 * The API class.
4 *
5 * @link http://wpmudev.com
6 * @since 1.0.0
7 * @author Joel James <[email protected]>
8 * @package WPMUDEV\Hub\Connector
9 */
10
11 namespace WPMUDEV\Hub\Connector;
12
13 use WP_Error;
14
15 /**
16 * Class API
17 */
18 class API {
19
20 use Singleton;
21
22 /**
23 * Check if member is logged in.
24 *
25 * @since 1.0.0
26 *
27 * @return bool
28 */
29 public function is_logged_in() {
30 $membership_type = Data::get()->membership_type();
31
32 return $this->has_api_key() && ! empty( $membership_type );
33 }
34
35 /**
36 * Check if an API key is set.
37 *
38 * @since 1.0.0
39 * @sice 1.0.6 Added force param.
40 *
41 * @param bool $force Force clear cache.
42 *
43 * @return bool
44 */
45 public function has_api_key( $force = false ) {
46 $key = $this->get_api_key( $force );
47
48 return ! empty( $key );
49 }
50
51 /**
52 * Get API key for current member.
53 *
54 * @since 1.0.0
55 * @sice 1.0.6 Added force param.
56 *
57 * @param bool $force Force clear cache.
58 *
59 * @return string
60 */
61 public function get_api_key( $force = false ) {
62 if ( defined( '\WPMUDEV_APIKEY' ) && \WPMUDEV_APIKEY ) {
63 return \WPMUDEV_APIKEY;
64 } else {
65 /**
66 * If forced, clear cache and get the value.
67 *
68 * Temporary workaround for cache plugin conflicts.
69 *
70 * @see https://incsub.atlassian.net/browse/DEF-2895
71 */
72 if ( $force ) {
73 if ( is_multisite() ) {
74 $network_id = get_current_network_id();
75 $cache_key = "$network_id:wpmudev_apikey";
76 wp_cache_delete( $cache_key, 'site-options' );
77 } else {
78 wp_cache_delete( 'wpmudev_apikey', 'options' );
79 }
80 }
81
82 // If 'clear_key' is present in URL then do not load the key from DB.
83 return get_site_option( 'wpmudev_apikey', '' );
84 }
85 }
86
87 /**
88 * Set API key for current member.
89 *
90 * @since 1.0.0
91 *
92 * @param string $key API key.
93 *
94 * @return bool
95 */
96 public function set_api_key( $key ) {
97 return update_site_option( 'wpmudev_apikey', $key );
98 }
99
100 /**
101 * Returns the full URL to the specified REST API endpoint.
102 *
103 * @since 1.0.0
104 *
105 * @param string $endpoint The endpoint to call on the server.
106 *
107 * @return string The full URL to the requested endpoint.
108 */
109 public function rest_url( $endpoint ) {
110 return Data::get()->server_url( 'api/dashboard/v2/' . $endpoint );
111 }
112
113 /**
114 * Returns the full URL to the specified REST API endpoint and includes
115 * the API key as last element in URL.
116 *
117 * Uses the function `rest_url()` to build the URL.
118 *
119 * @since 1.0.0
120 *
121 * @param string $endpoint The endpoint to call on the server.
122 *
123 * @return string The full URL to the requested endpoint.
124 */
125 public function rest_url_auth( $endpoint ) {
126 $api_key = $this->get_api_key();
127
128 // Append API key.
129 if ( false === strpos( $endpoint, '/' . $api_key ) ) {
130 $endpoint .= '/' . $api_key;
131 }
132
133 // Get full URL.
134 $url = $this->rest_url( $endpoint );
135
136 // Add hub site id if available.
137 $site_id = Data::get()->hub_site_id();
138 if ( ! empty( $site_id ) ) {
139 $url = add_query_arg( 'site_id', $site_id, $url );
140 }
141
142 return $url;
143 }
144
145 /**
146 * Contacts the API to sync the latest data from this site.
147 *
148 * Returns the membership status if things are working out.
149 * In case the API call fails the function returns boolean false and does
150 * not update the update
151 *
152 * @since 1.0.0
153 *
154 * @param bool $force Optional forces a sync.
155 * @param bool $auth_check Should check for API key.
156 *
157 * @return array|WP_Error
158 */
159 public function sync_site( $force = false, $auth_check = true ) {
160 global $wp_version;
161
162 // Only when logged in.
163 if ( $auth_check && ! $this->has_api_key( $force ) ) {
164 return new WP_Error(
165 'not_logged_in',
166 __( 'Not logged in.', 'wpmudev' )
167 );
168 }
169
170 // New request object.
171 $request = new Request();
172
173 // Last sync timestamp.
174 $last_run = Options::get( 'timestamp_sync', array() );
175
176 $data = array(
177 'call_version' => \WPMUDEV_HUB_CONNECTOR_VERSION,
178 'domain' => Data::get()->network_site_url(),
179 'blog_count' => is_multisite() ? get_blog_count() : 1,
180 'wp_version' => is_multisite() ? "WordPress Multisite $wp_version" : "WordPress $wp_version",
181 'projects' => wp_json_encode( Data::get()->wpmudev_projects() ),
182 'admin_url' => Data::get()->network_admin_url(),
183 'home_url' => Data::get()->network_home_url(),
184 'sso_status' => false,
185 'repo_updates' => wp_json_encode( array() ),
186 'packages' => wp_json_encode(
187 array(
188 'plugins' => Data::get()->plugins(),
189 'themes' => Data::get()->themes(),
190 )
191 ),
192 );
193
194 /**
195 * Filter hook to modify final API data.
196 *
197 * @param array $data Data.
198 */
199 $data = apply_filters( 'wpmudev_hub_connector_get_api_data', $data );
200
201 // Get a hash of the data to see if it changed.
202 $data_hash = md5( wp_json_encode( $data ) );
203
204 // Clear API cache if forcing an update.
205 if ( $force || empty( $last_run ) ) {
206 $data['call_version'] = microtime( true );
207 } else {
208 $membership_data = Data::get()->membership_data();
209 if ( ! empty( $membership_data['membership'] ) ) {
210 // This is the main check to prevent pinging unless the data is changed or 6 hrs have passed.
211 if ( isset( $last_run['hash'], $last_run['time'] ) && $last_run['hash'] === $data_hash && $last_run['time'] > ( time() - ( \HOUR_IN_SECONDS * 6 ) ) ) {
212 $this->maybe_log( '[WPMUDEV API] Skipped sync due to unchanged local data.' );
213
214 return $membership_data;
215 } elseif ( $last_run['fails'] ) { // Check for exponential backoff.
216 // 5, 25, 125, 625, 3125, 3600 max.
217 $backoff = min( pow( 5, $last_run['fails'] ), \HOUR_IN_SECONDS );
218 if ( $last_run['time'] > ( time() - $backoff ) ) {
219 $this->maybe_log( '[WPMUDEV API] Skipped sync due to API error exponential backoff.' );
220
221 return $membership_data;
222 }
223 }
224 }
225 }
226
227 // Make a hub sync request.
228 $response = $request->post( 'hub-sync', true, $data );
229
230 // Sync success.
231 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
232 // Get membership data.
233 $data = json_decode( wp_remote_retrieve_body( $response ), true );
234 if ( ! empty( $data['membership'] ) ) {
235 // Update membership data.
236 $this->update_membership_data( $data );
237
238 // Update sync timestamps.
239 Options::set(
240 'timestamp_sync',
241 array(
242 'time' => time(),
243 'hash' => $data_hash,
244 'fails' => 0,
245 )
246 );
247
248 $result = $data;
249 } else {
250 return new WP_Error(
251 'invalid_api_response',
252 __( 'Invalid remote API response.', 'wpmudev' )
253 );
254 }
255 } else {
256 // For network errors, perform exponential backoff.
257 Options::set(
258 'timestamp_sync',
259 array(
260 'time' => time(),
261 'fails' => 1,
262 )
263 );
264
265 $api_error = $this->get_api_error( $response );
266 // Format error messages.
267 $api_error = $this->format_error_messages( $api_error );
268
269 return new WP_Error(
270 $api_error['code'],
271 $api_error['message']
272 );
273 }
274
275 /**
276 * Action hook fired after a sync is completed.
277 *
278 * @since 1.0.0
279 */
280 do_action( 'wpmudev_hub_connector_sync_completed' );
281
282 // If first time sync.
283 if ( empty( $last_run ) ) {
284 /**
285 * Action hook fired after the first sync is completed.
286 *
287 * @since 1.0.0
288 */
289 do_action( 'wpmudev_hub_connector_first_sync_completed' );
290 }
291
292 return $result;
293 }
294
295 /**
296 * Logout and disconnect the site from Hub.
297 *
298 * @since 1.0.0
299 *
300 * @return array|WP_Error
301 */
302 public function logout() {
303 // Not logged in.
304 if ( ! $this->is_logged_in() ) {
305 return new WP_Error( 'not_logged_in', __( 'Not logged in.', 'wpmudev' ) );
306 }
307
308 // Reset settings.
309 Options::reset();
310 // Remove API key.
311 $this->set_api_key( '' );
312
313 // Do a sync to remove site.
314 $sync = $this->sync_site( true, false );
315
316 // Handle specific error.
317 if ( is_wp_error( $sync ) && 'invalid_api_response' === $sync->get_error_code() ) {
318 // For logout sync, membership data will be empty.
319 return array();
320 }
321
322 return $sync;
323 }
324
325 /**
326 * Get available teams for the authenticated user.
327 *
328 * This list is used to select the team after login.
329 *
330 * @since 1.0.0
331 *
332 * @param string $key API key.
333 *
334 * @return array|bool
335 */
336 public function get_hub_teams( $key ) {
337 $request = new Request();
338
339 // Sets up special auth header.
340 $request->add_header_argument( 'Authorization', $key );
341
342 // Send API request.
343 $response = $request->get( 'site-authenticate-teams' );
344
345 // Error.
346 if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
347 return false;
348 } else {
349 // Get team list.
350 $data = json_decode( wp_remote_retrieve_body( $response ), true );
351 if ( isset( $data['data'] ) ) {
352 return $data['data'];
353 }
354 }
355
356 return array();
357 }
358
359 /**
360 * Get the currently logged in member user profile data.
361 *
362 * If not found in db, get it from the API.
363 *
364 * @since 1.0.0
365 *
366 * @param bool $force Force update from API.
367 *
368 * @return array|WP_Error
369 */
370 public function get_profile( $force = false ) {
371 // Only when API key is available.
372 if ( ! $this->has_api_key() ) {
373 return new WP_Error(
374 'not_logged_in',
375 __( 'Not logged in.', 'wpmudev' )
376 );
377 }
378
379 $profile = Options::get( 'profile_data' );
380
381 if ( ! empty( $profile ) && ! $force ) {
382 return $profile;
383 }
384
385 // New request object.
386 $request = new Request();
387
388 // Make an API request.
389 $response = $request->get( 'user-info', true );
390
391 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
392 $data = json_decode( wp_remote_retrieve_body( $response ), true );
393 if ( isset( $data['profile'] ) ) {
394 // Set profile data.
395 Options::set( 'profile_data', $data['profile'] );
396
397 return $data['profile'];
398 } else {
399 return new WP_Error(
400 'api_request_error',
401 __( 'Error unserializing remote response.', 'wpmudev' )
402 );
403 }
404 } else {
405 $api_error = $this->get_api_error( $response );
406
407 return new WP_Error(
408 $api_error['code'],
409 $api_error['message']
410 );
411 }
412 }
413
414 /**
415 * Parse API error and get a readable error message.
416 *
417 * @since 1.0.0
418 *
419 * @param mixed $response API response.
420 *
421 * @return array
422 */
423 private function get_api_error( $response ) {
424 // Default error code is 500.
425 $error_code = wp_remote_retrieve_response_code( $response );
426 if ( ! $error_code ) {
427 $error_code = 500;
428 }
429
430 $error = array(
431 'code' => 'api_error',
432 'message' => '',
433 );
434
435 // Attempt to retrieve http response body.
436 $body = is_array( $response ) ? wp_remote_retrieve_body( $response ) : false;
437
438 // Get error message.
439 if ( is_scalar( $response ) ) {
440 $error['message'] = $response;
441 } elseif ( is_wp_error( $response ) ) {
442 $error['message'] = $response->get_error_message();
443 } elseif ( is_array( $response ) && ! empty( $body ) ) {
444 $data = json_decode( wp_remote_retrieve_body( $response ), true );
445 if ( is_array( $data ) ) {
446 if ( ! empty( $data['message'] ) ) {
447 $error['message'] = $data['message'];
448 }
449 if ( ! empty( $data['code'] ) ) {
450 $error['code'] = $data['code'];
451 }
452 }
453 }
454
455 $url = '(unknown URL)';
456 if ( is_array( $response ) && isset( $response['request_url'] ) ) {
457 $url = $response['request_url'];
458 }
459
460 if ( empty( $error['message'] ) ) {
461 $error['message'] = sprintf(
462 'HTTP Error: %s "%s"',
463 $error_code,
464 wp_remote_retrieve_response_message( $response )
465 );
466 }
467
468 if ( defined( '\WPMUDEV_API_DEBUG' ) && \WPMUDEV_API_DEBUG ) {
469 $trace = debug_backtrace();
470 $caller = array();
471 $last_line = '';
472 foreach ( $trace as $level => $item ) {
473 if ( ! isset( $item['class'] ) ) {
474 $item['class'] = '';
475 }
476 if ( ! isset( $item['type'] ) ) {
477 $item['type'] = '';
478 }
479 if ( ! isset( $item['function'] ) ) {
480 $item['function'] = '<function>';
481 }
482 if ( ! isset( $item['line'] ) ) {
483 $item['line'] = '?';
484 }
485
486 if ( $level > 0 ) {
487 $caller[] = $item['class'] . $item['type'] . $item['function'] . ':' . $last_line;
488 }
489 $last_line = $item['line'];
490 }
491 $caller_dump = "\n\t# " . implode( "\n\t# ", $caller );
492
493 if ( is_array( $response ) && isset( $response['request_url'] ) ) {
494 $caller_dump = "\n\tURL: " . $response['request_url'] . $caller_dump;
495 }
496
497 // Log the error to PHP error log.
498 error_log(
499 sprintf(
500 '[WPMUDEV API Error] %s | %s (%s [%s]) %s',
501 \WPMUDEV_HUB_CONNECTOR_VERSION,
502 $error['message'],
503 $url,
504 $error_code,
505 $caller_dump
506 ),
507 0
508 );
509 }
510
511 // If error was "invalid API key" then log out the user. (we don't call logout here to avoid infinite loop).
512 if ( 401 == $error_code && ! defined( '\WPMUDEV_APIKEY' ) && ! defined( '\WPMUDEV_OVERRIDE_LOGOUT' ) ) {
513 $this->set_api_key( '' );
514 }
515
516 return $error;
517 }
518
519 /**
520 * Update membership data from API data.
521 *
522 * @since 1.0.0
523 *
524 * @param array $data Data to update.
525 *
526 * @return void
527 */
528 private function update_membership_data( $data ) {
529 if (
530 isset( $data['membership'] ) &&
531 empty( $data['membership'] ) &&
532 ! defined( '\WPMUDEV_APIKEY' ) && $this->get_api_key()
533 ) {
534 // Clear API key.
535 $this->set_api_key( '' );
536 }
537
538 // Update membership data.
539 Options::set( 'membership_data', $data );
540 }
541
542 /**
543 * Write data to error log if log is enabled.
544 *
545 * @since 1.0.0
546 *
547 * @param string $data Data to log.
548 *
549 * @return void
550 */
551 private function maybe_log( $data ) {
552 // Only if logging is enabled.
553 if ( defined( '\WPMUDEV_API_DEBUG' ) && \WPMUDEV_API_DEBUG ) {
554 error_log( $data );
555 }
556 }
557
558 /**
559 * Format error messages before showing to public.
560 *
561 * @since 1.0.0
562 *
563 * @param array $error Error data.
564 *
565 * @return array
566 */
567 private function format_error_messages( $error ) {
568 // Need both code and message.
569 if ( ! isset( $error['code'], $error['message'] ) ) {
570 return $error;
571 }
572
573 // Already registered error.
574 if ( 'already_registered' === $error['code'] ) {
575 $error['message'] = sprintf(
576 // translators: %s Support URL.
577 __( 'This site is currently registered to a different user. Please <a target="_blank" href="%s">contact support for assistance</a>.', 'wpmudev' ),
578 Data::get()->server_url( 'hub/support/' )
579 );
580 }
581
582 return $error;
583 }
584 }
585