PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
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 6.3.0, at extra/hub-connector/inc/class-api.php

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