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

582 lines 14.2 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 ( ! empty( $data['membership'] ) ) {
244 // Update membership data.
245 $this->update_membership_data( $data );
246
247 // Update sync timestamps.
248 Options::set(
249 'timestamp_sync',
250 array(
251 'time' => time(),
252 'hash' => $data_hash,
253 'fails' => 0,
254 )
255 );
256
257 $result = $data;
258 } else {
259 return new WP_Error(
260 'invalid_api_response',
261 __( 'Invalid remote API response.', 'wpmudev' )
262 );
263 }
264 } else {
265 // For network errors, perform exponential backoff.
266 Options::set(
267 'timestamp_sync',
268 array(
269 'time' => time(),
270 'fails' => 1,
271 )
272 );
273
274 $api_error = $this->get_api_error( $response );
275 // Format error messages.
276 $api_error = $this->format_error_messages( $api_error );
277
278 return new WP_Error(
279 $api_error['code'],
280 $api_error['message']
281 );
282 }
283
284 /**
285 * Action hook fired after a sync is completed.
286 *
287 * @since 1.0.0
288 */
289 do_action( 'wpmudev_hub_connector_sync_completed' );
290
291 // If first time sync.
292 if ( empty( $last_run ) ) {
293 /**
294 * Action hook fired after the first sync is completed.
295 *
296 * @since 1.0.0
297 */
298 do_action( 'wpmudev_hub_connector_first_sync_completed' );
299 }
300
301 return $result;
302 }
303
304 /**
305 * Logout and disconnect the site from Hub.
306 *
307 * @since 1.0.0
308 *
309 * @return array|WP_Error
310 */
311 public function logout() {
312 // Not logged in.
313 if ( ! $this->is_logged_in() ) {
314 return new WP_Error( 'not_logged_in', __( 'Not logged in.', 'wpmudev' ) );
315 }
316
317 // Reset settings.
318 Options::reset();
319 // Remove API key.
320 $this->set_api_key( '' );
321
322 // Do a sync to remove site.
323 $sync = $this->sync_site( true, false );
324
325 // Handle specific error.
326 if ( is_wp_error( $sync ) && 'invalid_api_response' === $sync->get_error_code() ) {
327 // For logout sync, membership data will be empty.
328 return array();
329 }
330
331 return $sync;
332 }
333
334 /**
335 * Get available teams for the authenticated user.
336 *
337 * This list is used to select the team after login.
338 *
339 * @since 1.0.0
340 *
341 * @param string $key API key.
342 *
343 * @return array|bool
344 */
345 public function get_hub_teams( $key ) {
346 $request = new Request();
347
348 // Sets up special auth header.
349 $request->add_header_argument( 'Authorization', $key );
350
351 // Send API request.
352 $response = $request->get( 'site-authenticate-teams' );
353
354 // Error.
355 if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
356 return false;
357 } else {
358 // Get team list.
359 $data = json_decode( wp_remote_retrieve_body( $response ), true );
360 if ( isset( $data['data'] ) ) {
361 return $data['data'];
362 }
363 }
364
365 return array();
366 }
367
368 /**
369 * Get the currently logged in member user profile data.
370 *
371 * If not found in db, get it from the API.
372 *
373 * @since 1.0.0
374 *
375 * @param bool $force Force update from API.
376 *
377 * @return array|WP_Error
378 */
379 public function get_profile( $force = false ) {
380 // Only when API key is available.
381 if ( ! $this->has_api_key() ) {
382 return new WP_Error(
383 'not_logged_in',
384 __( 'Not logged in.', 'wpmudev' )
385 );
386 }
387
388 $profile = Options::get( 'profile_data' );
389
390 if ( ! empty( $profile ) && ! $force ) {
391 return $profile;
392 }
393
394 // New request object.
395 $request = new Request();
396
397 // Make an API request.
398 $response = $request->get( 'user-info', true );
399
400 if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
401 $data = json_decode( wp_remote_retrieve_body( $response ), true );
402 if ( isset( $data['profile'] ) ) {
403 // Set profile data.
404 Options::set( 'profile_data', $data['profile'] );
405
406 return $data['profile'];
407 } else {
408 return new WP_Error(
409 'api_request_error',
410 __( 'Error unserializing remote response.', 'wpmudev' )
411 );
412 }
413 } else {
414 $api_error = $this->get_api_error( $response );
415
416 return new WP_Error(
417 $api_error['code'],
418 $api_error['message']
419 );
420 }
421 }
422
423 /**
424 * Parse API error and get a readable error message.
425 *
426 * @since 1.0.0
427 *
428 * @param mixed $response API response.
429 *
430 * @return array
431 */
432 private function get_api_error( $response ) {
433 // Default error code is 500.
434 $error_code = wp_remote_retrieve_response_code( $response );
435 if ( ! $error_code ) {
436 $error_code = 500;
437 }
438
439 $error = array(
440 'code' => 'api_error',
441 'message' => '',
442 );
443
444 // Attempt to retrieve http response body.
445 $body = is_array( $response ) ? wp_remote_retrieve_body( $response ) : false;
446
447 // Get error message.
448 if ( is_scalar( $response ) ) {
449 $error['message'] = $response;
450 } elseif ( is_wp_error( $response ) ) {
451 $error['message'] = $response->get_error_message();
452 } elseif ( is_array( $response ) && ! empty( $body ) ) {
453 $data = json_decode( wp_remote_retrieve_body( $response ), true );
454 if ( is_array( $data ) ) {
455 if ( ! empty( $data['message'] ) ) {
456 $error['message'] = $data['message'];
457 }
458 if ( ! empty( $data['code'] ) ) {
459 $error['code'] = $data['code'];
460 }
461 }
462 }
463
464 $url = '(unknown URL)';
465 if ( is_array( $response ) && isset( $response['request_url'] ) ) {
466 $url = $response['request_url'];
467 }
468
469 if ( empty( $error['message'] ) ) {
470 $error['message'] = sprintf(
471 'HTTP Error: %s "%s"',
472 $error_code,
473 wp_remote_retrieve_response_message( $response )
474 );
475 }
476
477 // Only enabled with constants.
478 // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
479 // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log
480 if ( defined( '\WPMUDEV_API_DEBUG' ) && \WPMUDEV_API_DEBUG ) {
481 $trace = wp_debug_backtrace_summary( null, null, false );
482 $caller_dump = "\n\t# " . implode( "\n\t# ", $trace );
483
484 if ( is_array( $response ) && isset( $response['request_url'] ) ) {
485 $caller_dump = "\n\tURL: " . $response['request_url'] . $caller_dump;
486 }
487
488 // Log the error to PHP error log.
489 error_log(
490 sprintf(
491 '[WPMUDEV API Error] %s | %s (%s [%s]) %s',
492 \WPMUDEV_HUB_CONNECTOR_VERSION,
493 $error['message'],
494 $url,
495 $error_code,
496 $caller_dump
497 ),
498 0
499 );
500 }
501 // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_wp_debug_backtrace_summary
502 // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_error_log
503
504 // If error was "invalid API key" then log out the user. (we don't call logout here to avoid infinite loop).
505 if ( 401 === (int) $error_code && ! defined( '\WPMUDEV_APIKEY' ) && ! defined( '\WPMUDEV_OVERRIDE_LOGOUT' ) ) {
506 $this->set_api_key( '' );
507 }
508
509 return $error;
510 }
511
512 /**
513 * Update membership data from API data.
514 *
515 * @since 1.0.0
516 *
517 * @param array $data Data to update.
518 *
519 * @return void
520 */
521 private function update_membership_data( $data ) {
522 if (
523 isset( $data['membership'] )
524 && empty( $data['membership'] )
525 && ! defined( '\WPMUDEV_APIKEY' )
526 && $this->get_api_key()
527 ) {
528 // Clear API key.
529 $this->set_api_key( '' );
530 }
531
532 // Update membership data.
533 Options::set( 'membership_data', $data );
534 }
535
536 /**
537 * Write data to error log if log is enabled.
538 *
539 * @since 1.0.0
540 *
541 * @param string $data Data to log.
542 *
543 * @return void
544 */
545 private function maybe_log( $data ) {
546 // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_error_log
547
548 // Only if logging is enabled.
549 if ( defined( '\WPMUDEV_API_DEBUG' ) && \WPMUDEV_API_DEBUG ) {
550 error_log( $data );
551 }
552 // phpcs:enable WordPress.PHP.DevelopmentFunctions.error_log_error_log
553 }
554
555 /**
556 * Format error messages before showing to public.
557 *
558 * @since 1.0.0
559 *
560 * @param array $error Error data.
561 *
562 * @return array
563 */
564 private function format_error_messages( $error ) {
565 // Need both code and message.
566 if ( ! isset( $error['code'], $error['message'] ) ) {
567 return $error;
568 }
569
570 // Already registered error.
571 if ( 'already_registered' === $error['code'] ) {
572 $error['message'] = sprintf(
573 // translators: %s Support URL.
574 __( 'This site is currently registered to a different user. Please <a target="_blank" href="%s">contact support for assistance</a>.', 'wpmudev' ),
575 Data::get()->server_url( 'hub/support/' )
576 );
577 }
578
579 return $error;
580 }
581 }
582