%PDF- %PDF-
Direktori : /home/eirtvhdf/cloudsslamllc.com/wp-content/plugins/litespeed-cache/src/ |
Current File : /home/eirtvhdf/cloudsslamllc.com/wp-content/plugins/litespeed-cache/src/cloud.cls.php |
<?php /** * Cloud service cls * * @since 3.0 */ namespace LiteSpeed; defined( 'WPINC' ) || exit; class Cloud extends Base { const LOG_TAG = '❄️'; const CLOUD_SERVER = 'https://api.quic.cloud'; const CLOUD_IPS = 'https://quic.cloud/ips'; const CLOUD_SERVER_DASH = 'https://my.quic.cloud'; const CLOUD_SERVER_WP = 'https://wpapi.quic.cloud'; const SVC_D_NODES = 'd/nodes'; const SVC_D_SYNC_CONF = 'd/sync_conf'; const SVC_D_USAGE = 'd/usage'; const SVC_D_SETUP_TOKEN = 'd/get_token'; const SVC_D_DEL_CDN_DNS = 'd/del_cdn_dns'; const SVC_PAGE_OPTM = 'page_optm'; const SVC_CCSS = 'ccss'; const SVC_UCSS = 'ucss'; const SVC_VPI = 'vpi'; const SVC_LQIP = 'lqip'; const SVC_QUEUE = 'queue'; const SVC_IMG_OPTM = 'img_optm'; const SVC_HEALTH = 'health'; const SVC_CDN = 'cdn'; const BM_IMG_OPTM_PRIO = 16; const BM_IMG_OPTM_JUMBO_GROUP = 32; const IMG_OPTM_JUMBO_GROUP = 1000; const IMG_OPTM_DEFAULT_GROUP = 200; const IMGOPTM_TAKEN = 'img_optm-taken'; const TTL_NODE = 3; // Days before node expired const EXPIRATION_REQ = 300; // Seconds of min interval between two unfinished requests const EXPIRATION_TOKEN = 900; // Min intval to request a token 15m const TTL_IPS = 3; // Days for node ip list cache const API_REPORT = 'wp/report' ; const API_NEWS = 'news'; const API_VER = 'ver'; const API_BETA_TEST = 'beta_test'; private static $CENTER_SVC_SET = array( self::SVC_D_NODES, self::SVC_D_SYNC_CONF, self::SVC_D_USAGE, // self::API_NEWS, self::API_REPORT, // self::API_VER, // self::API_BETA_TEST, self::SVC_D_SETUP_TOKEN, self::SVC_D_DEL_CDN_DNS, ); private static $WP_SVC_SET = array( self::API_NEWS, self::API_VER, self::API_BETA_TEST, ); // No api key needed for these services private static $_PUB_SVC_SET = array( self::API_NEWS, self::API_REPORT, self::API_VER, self::API_BETA_TEST, ); private static $_QUEUE_SVC_SET = array( self::SVC_UCSS, self::SVC_VPI, ); public static $SERVICES_LOAD_CHECK = array( self::SVC_CCSS, // self::SVC_UCSS, // self::SVC_VPI, self::SVC_LQIP, self::SVC_HEALTH, ); public static $SERVICES = array( self::SVC_IMG_OPTM, self::SVC_PAGE_OPTM, self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI, self::SVC_LQIP, self::SVC_CDN, self::SVC_HEALTH, // self::SVC_QUEUE, ); const TYPE_CLEAR_PROMO = 'clear_promo'; const TYPE_REDETECT_CLOUD = 'redetect_cloud'; const TYPE_CLEAR_CLOUD = 'clear_cloud'; const TYPE_GEN_KEY = 'gen_key'; const TYPE_LINK = 'link'; const TYPE_SYNC_USAGE = 'sync_usage'; private $_api_key; private $_setup_token; protected $_summary; /** * Init * * @since 3.0 */ public function __construct() { $this->_api_key = $this->conf( self::O_API_KEY ); $this->_setup_token = $this->conf( self::O_QC_TOKEN ); $this->_summary = self::get_summary(); } /** * Show latest commit version always if is on dev * * @since 3.0 */ public function check_dev_version() { if ( ! preg_match( '/[^\d\.]/', Core::VER ) ) { return; } $last_check = empty( $this->_summary[ 'last_request.' . self::API_VER ] ) ? 0 : $this->_summary[ 'last_request.' . self::API_VER ] ; if ( time() - $last_check > 86400 ) { $auto_v = self::version_check( 'dev' ); if ( ! empty( $auto_v[ 'dev' ] ) ) { self::save_summary( array( 'version.dev' => $auto_v[ 'dev' ] ) ); } } if ( empty( $this->_summary[ 'version.dev' ] ) ) { return; } self::debug( 'Latest dev version ' . $this->_summary[ 'version.dev' ] ); if ( version_compare( $this->_summary[ 'version.dev' ], Core::VER, '<=' ) ) { return; } // Show the dev banner require_once LSCWP_DIR . 'tpl/banner/new_version_dev.tpl.php'; } /** * Check latest version * * @since 2.9 * @access public */ public static function version_check( $src = false ) { $req_data = array( 'v' => defined( 'LSCWP_CUR_V' ) ? LSCWP_CUR_V : '', 'src' => $src, ); if ( defined( 'LITESPEED_ERR' ) ) { $req_data[ 'err' ] = base64_encode( ! is_string( LITESPEED_ERR ) ? json_encode( LITESPEED_ERR ) : LITESPEED_ERR ) ; } $data = self::get( self::API_VER, $req_data ); return $data; } /** * Show latest news * * @since 3.0 */ public function news() { $this->_update_news(); if ( empty( $this->_summary[ 'news.new' ] ) ) { return; } if ( ! empty( $this->_summary[ 'news.plugin' ] ) && Activation::cls()->dash_notifier_is_plugin_active( $this->_summary[ 'news.plugin' ] ) ) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_news.tpl.php' ; } /** * Update latest news * * @since 2.9.9.1 */ private function _update_news() { if ( ! empty( $this->_summary[ 'news.utime' ] ) && time() - $this->_summary[ 'news.utime' ] < 86400 * 7 ) { return; } self::save_summary( array( 'news.utime' => time() ) ); $data = self::get( self::API_NEWS ); if ( empty( $data[ 'id' ] ) ) { return; } // Save news if ( ! empty( $this->_summary[ 'news.id' ] ) && $this->_summary[ 'news.id' ] == $data[ 'id' ] ) { return; } $this->_summary[ 'news.id' ] = $data[ 'id' ]; $this->_summary[ 'news.plugin' ] = ! empty( $data[ 'plugin' ] ) ? $data[ 'plugin' ] : ''; $this->_summary[ 'news.title' ] = ! empty( $data[ 'title' ] ) ? $data[ 'title' ] : ''; $this->_summary[ 'news.content' ] = ! empty( $data[ 'content' ] ) ? $data[ 'content' ] : ''; $this->_summary[ 'news.zip' ] = ! empty( $data[ 'zip' ] ) ? $data[ 'zip' ] : ''; $this->_summary[ 'news.new' ] = 1; if ( $this->_summary[ 'news.plugin' ] ) { $plugin_info = Activation::cls()->dash_notifier_get_plugin_info( $this->_summary[ 'news.plugin' ] ); if ( $plugin_info && ! empty( $plugin_info->name ) ) { $this->_summary[ 'news.plugin_name' ] = $plugin_info->name; } } self::save_summary(); } /** * Check if contains a package in a service or not * * @since 4.0 */ public function has_pkg( $service, $pkg ) { if ( ! empty( $this->_summary[ 'usage.' . $service ][ 'pkgs' ] ) && $this->_summary[ 'usage.' . $service ][ 'pkgs' ] & $pkg ) { return true; } return false; } /** * Get allowance of current service * * @since 3.0 * @access private */ public function allowance( $service, &$err = false ) { // Only auto sync usage at most one time per day if ( empty( $this->_summary[ 'last_request.' . self::SVC_D_USAGE ] ) || time() - $this->_summary[ 'last_request.' . self::SVC_D_USAGE ] > 86400 ) { $this->sync_usage(); } if ( in_array( $service, array( self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI ) ) ) { // @since 4.2 $service = self::SVC_PAGE_OPTM; } if ( empty( $this->_summary[ 'usage.' . $service ] ) ) { return 0; } $usage = $this->_summary[ 'usage.' . $service ]; // Image optm is always free $allowance_max = 0; if ( $service == self::SVC_IMG_OPTM ) { $allowance_max = self::IMG_OPTM_DEFAULT_GROUP; if ( ! empty( $usage[ 'pkgs' ] ) && $usage[ 'pkgs' ] & self::BM_IMG_OPTM_JUMBO_GROUP ) { $allowance_max = self::IMG_OPTM_JUMBO_GROUP; } } $allowance = $usage[ 'quota' ] - $usage[ 'used' ]; $err = 'out_of_quota'; if ( $allowance > 0 ) { if ( $allowance_max && $allowance_max < $allowance ) { $allowance = $allowance_max; } // Daily limit @since 4.2 if ( isset( $usage[ 'remaining_daily_quota' ] ) && $usage[ 'remaining_daily_quota' ] >= 0 && $usage[ 'remaining_daily_quota' ] < $allowance ) { $allowance = $usage[ 'remaining_daily_quota' ]; if ( ! $allowance ) { $err = 'out_of_daily_quota'; } } return $allowance; } // Check Pay As You Go balance if ( empty( $usage[ 'pag_bal' ] ) ) { return $allowance_max; } if ( $allowance_max && $allowance_max < $usage[ 'pag_bal' ] ) { return $allowance_max; } return $usage[ 'pag_bal' ]; } /** * Sync Cloud usage summary data * * @since 3.0 * @access public */ public function sync_usage() { $usage = $this->_post( self::SVC_D_USAGE ); if ( ! $usage ) { return; } self::debug( 'sync_usage ' . json_encode( $usage ) ); foreach ( self::$SERVICES as $v ) { $this->_summary[ 'usage.' . $v ] = ! empty( $usage[ $v ] ) ? $usage[ $v ] : false; } self::save_summary(); return $this->_summary; } /** * Clear all existing cloud nodes for future reconnect * * @since 3.0 * @access public */ public function clear_cloud() { foreach ( self::$SERVICES as $service ) { if ( isset( $this->_summary[ 'server.' . $service ] ) ) { unset( $this->_summary[ 'server.' . $service ] ); } if ( isset( $this->_summary[ 'server_date.' . $service ] ) ) { unset( $this->_summary[ 'server_date.' . $service ] ); } } self::save_summary(); self::debug( 'Cleared all local service node caches' ); } /** * ping clouds to find the fastest node * * @since 3.0 * @access public */ public function detect_cloud( $service, $force = false ) { if ( in_array( $service, self::$CENTER_SVC_SET ) ) { return self::CLOUD_SERVER; } if ( in_array( $service, self::$WP_SVC_SET ) ) { return self::CLOUD_SERVER_WP; } // Check if the stored server needs to be refreshed if ( ! $force ) { if ( ! empty( $this->_summary[ 'server.' . $service ] ) && ! empty( $this->_summary[ 'server_date.' . $service ] ) && $this->_summary[ 'server_date.' . $service ] > time() - 86400 * self::TTL_NODE ) { return $this->_summary[ 'server.' . $service ]; } } if ( ! $service || ! in_array( $service, self::$SERVICES ) ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ': ' . $service; Admin_Display::error( $msg ); return false; } // Send request to Quic Online Service $json = $this->_post( self::SVC_D_NODES, array( 'svc' => $this->_maybe_queue( $service ) ) ); // Check if get list correctly if ( empty( $json[ 'list' ] ) || ! is_array( $json[ 'list' ] ) ) { self::debug( 'request cloud list failed: ', $json ); if ( $json ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . json_encode( $json ); Admin_Display::error( $msg ); } return false; } // Ping closest cloud $speed_list = array(); foreach ( $json[ 'list' ] as $v ) { // Exclude possible failed 503 nodes if ( ! empty( $this->_summary['disabled_node'] ) && ! empty($this->_summary['disabled_node'][$v]) && time() - $this->_summary['disabled_node'][$v] < 86400 ) { continue; } $speed_list[ $v ] = Utility::ping( $v ); } if ( ! $speed_list ) { self::debug( 'nodes are in 503 failed nodes' ); return false; } $min = min( $speed_list ); if ( $min == 99999 ) { self::debug( 'failed to ping all clouds' ); return false; } // Random pick same time range ip (230ms 250ms) $range_len = strlen( $min ); $range_num = substr( $min, 0, 1 ); $valid_clouds = array(); foreach ($speed_list as $node => $speed ) { if ( strlen( $speed ) == $range_len && substr( $speed, 0, 1 ) == $range_num ) { $valid_clouds[] = $node; } // Append the lower speed ones else if ( $speed < $min * 4 ) { $valid_clouds[] = $node; } } if ( ! $valid_clouds ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . __( 'No available Cloud Node.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } self::debug( 'Closest nodes list', $valid_clouds ); // Check server load if ( in_array( $service, self::$SERVICES_LOAD_CHECK ) ) { $valid_cloud_loads = array(); foreach ( $valid_clouds as $k => $v ) { $response = wp_remote_get( $v, array( 'timeout' => 5, 'sslverify' => true ) ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to do load checker: ' . $error_message ); continue; } $curr_load = json_decode( $response[ 'body' ], true ); if ( ! empty( $curr_load[ '_res' ] ) && $curr_load[ '_res' ] == 'ok' && isset( $curr_load[ 'load' ] ) ) { $valid_cloud_loads[ $v ] = $curr_load[ 'load' ]; } } if ( ! $valid_cloud_loads ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ": [Service] $service [Info] " . __( 'No available Cloud Node after checked server load.', 'litespeed-cache' ); Admin_Display::error( $msg ); return false; } self::debug( 'Closest nodes list after load check', $valid_cloud_loads ); $qualified_list = array_keys( $valid_cloud_loads, min( $valid_cloud_loads ) ); } else { $qualified_list = $valid_clouds; } $closest = $qualified_list[ array_rand( $qualified_list ) ]; self::debug( 'Chose node: ' . $closest ); // store data into option locally $this->_summary[ 'server.' . $service ] = $closest; $this->_summary[ 'server_date.' . $service ] = time(); self::save_summary(); return $this->_summary[ 'server.' . $service ]; } /** * May need to convert to queue service */ private function _maybe_queue( $service ) { if ( in_array( $service, self::$_QUEUE_SVC_SET ) ) return self::SVC_QUEUE; return $service; } /** * Get data from QUIC cloud server * * @since 3.0 * @access public */ public static function get( $service, $data = array() ) { $instance = self::cls(); return $instance->_get( $service, $data ); } /** * Get data from QUIC cloud server * * @since 3.0 * @access private */ private function _get( $service, $data = false ) { $service_tag = $service; if ( ! empty( $data[ 'action' ] ) ) { $service_tag .= '-' . $data[ 'action' ]; } if ( ! $this->_maybe_cloud( $service_tag ) ) { return; } $server = $this->detect_cloud( $service ); if ( ! $server ) { return; } $url = $server . '/' . $service; $param = array( 'site_url' => home_url(), 'domain_key' => $this->_api_key, 'main_domain' => ! empty( $this->_summary[ 'main_domain' ] ) ? $this->_summary[ 'main_domain' ] : '', 'ver' => Core::VER, ); if ( $data ) { $param[ 'data' ] = $data; } $url .= '?' . http_build_query( $param ); self::debug( 'getting from : ' . $url ); self::save_summary( array( 'curr_request.' . $service_tag => time() ) ); $response = wp_remote_get( $url, array( 'timeout' => 15, 'sslverify' => true ) ); return $this->_parse_response( $response, $service, $service_tag, $server ); } /** * Check if is able to do cloud request or not * * @since 3.0 * @access private */ private function _maybe_cloud( $service_tag ) { $home_url = home_url(); if ( ! wp_http_validate_url( $home_url ) ) { return false; } /** @since 5.0 If in valid err_domains, bypass request */ if ( $this->_is_err_domain( $home_url ) ) { return false; } // we don't want the `img_optm-taken` to fail at any given time if ( $service_tag == self::IMGOPTM_TAKEN ) { return true; } if ( $service_tag == self::SVC_D_SYNC_CONF && $this->_setup_token && ! $this->_api_key ) { self::debug( "Skip sync conf if API key is not available yet." ); return false; } $expiration_req = self::EXPIRATION_REQ; // Limit frequent unfinished request to 5min $timestamp_tag = 'curr_request.'; if ( $service_tag == self::SVC_IMG_OPTM . '-' . Img_Optm::TYPE_NEW_REQ ) { $timestamp_tag = 'last_request.'; if ( $this->has_pkg( self::SVC_IMG_OPTM, self::BM_IMG_OPTM_PRIO ) ) { $expiration_req /= 10; } } else { // For all other requests, if is under debug mode, will always allow if ( $this->conf( self::O_DEBUG ) && $this->_api_key ) { return true; } } if ( ! empty( $this->_summary[ $timestamp_tag . $service_tag ] ) ) { $expired = $this->_summary[ $timestamp_tag . $service_tag ] + $expiration_req - time(); if ( $expired > 0 ) { self::debug( "❌ try [$service_tag] after $expired seconds" ); if ( $service_tag !== self::API_VER ) { $msg = __( 'Cloud Error', 'litespeed-cache' ) . ': ' . sprintf( __( 'Please try after %1$s for service %2$s.', 'litespeed-cache' ), Utility::readable_time( $expired, 0, true ), '<code>' . $service_tag . '</code>' ); Admin_Display::error( array( 'cloud_trylater' => $msg ) ); } return false; } } if ( in_array( $service_tag, self::$_PUB_SVC_SET ) ) { return true; } if ( ! $this->_api_key ) { Admin_Display::error( Error::msg( 'lack_of_api_key' ) ); return false; } return true; } /** * Post data to QUIC.cloud server * * @since 3.0 * @access public */ public static function post( $service, $data = false, $time_out = false ) { $instance = self::cls(); return $instance->_post( $service, $data, $time_out ); } /** * Post data to cloud server * * @since 3.0 * @access private */ private function _post( $service, $data = false, $time_out = false ) { $service_tag = $service; if ( ! empty( $data[ 'action' ] ) ) { $service_tag .= '-' . $data[ 'action' ]; } if ( ! $this->_maybe_cloud( $service_tag ) ) { return; } $server = $this->detect_cloud( $service ); if ( ! $server ) { return; } $url = $server . '/' . $this->_maybe_queue( $service ); self::debug( 'posting to : ' . $url ); if ( $data ) { $data[ 'service_type' ] = $service; // For queue distribution usage } $param = array( 'site_url' => home_url(), 'domain_key' => $this->_api_key, 'main_domain' => ! empty( $this->_summary[ 'main_domain' ] ) ? $this->_summary[ 'main_domain' ] : '', 'ver' => Core::VER, 'data' => $data, ); self::save_summary( array( 'curr_request.' . $service_tag => time() ) ); $response = wp_remote_post( $url, array( 'body' => $param, 'timeout' => $time_out ?: 15, 'sslverify' => true ) ); return $this->_parse_response( $response, $service, $service_tag, $server ); } /** * Parse response JSON * Mark the request successful if the response status is ok * * @since 3.0 */ private function _parse_response( $response, $service, $service_tag, $server ) { if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to request: ' . $error_message ); if ( $service !== self::API_VER ) { $msg = __( 'Failed to request via WordPress', 'litespeed-cache' ) . ': ' . $error_message . " [server] $server [service] $service"; Admin_Display::error( $msg ); // Tmp disabled this node from reusing in 1 day if (empty($this->_summary['disabled_node'])) $this->_summary['disabled_node'] = array(); $this->_summary['disabled_node'][$server] = time(); self::save_summary(); // Force redetect node self::debug( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); } return; } $json = json_decode( $response[ 'body' ], true ); if ( ! is_array( $json ) ) { self::debug( 'failed to decode response json: ' . $response[ 'body' ] ); if ( $service !== self::API_VER ) { $msg = __( 'Failed to request via WordPress', 'litespeed-cache' ) . ': ' . $response[ 'body' ] . " [server] $server [service] $service"; Admin_Display::error( $msg ); // Tmp disabled this node from reusing in 1 day if (empty($this->_summary['disabled_node'])) $this->_summary['disabled_node'] = array(); $this->_summary['disabled_node'][$server] = time(); self::save_summary(); // Force redetect node self::debug( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); } return; } if ( ! empty( $json[ '_code' ] ) ) { if ( $json[ '_code' ] == 'heavy_load' || $json[ '_code' ] == 'redetect_node' ) { // Force redetect node self::debug( 'Node redetecting node [svc] ' . $service ); Admin_Display::info( __( 'Redetected node', 'litespeed-cache' ) . ': ' . Error::msg( $json[ '_code' ] ) ); $this->detect_cloud( $service, true ); } } if ( ! empty( $json[ '_503' ] ) ) { self::debug( 'service 503 unavailable temporarily. ' . $json[ '_503' ] ); $msg = __( 'We are working hard to improve your online service experience. The service will be unavailable while we work. We apologize for any inconvenience.', 'litespeed-cache' ); $msg .= ' ' . $json[ '_503' ] . " [server] $server [service] $service"; Admin_Display::error( $msg ); // Force redetect node self::debug( 'Node error, redetecting node [svc] ' . $service ); $this->detect_cloud( $service, true ); return; } list( $json, $return ) = $this->extract_msg( $json, $service, $server ); if ( $return ) return; self::save_summary( array( 'last_request.' . $service_tag => $this->_summary[ 'curr_request.' . $service_tag ], 'curr_request.' . $service_tag => 0 )); if ( $json ) { self::debug2( 'response ok', $json ); } else { self::debug2( 'response ok' ); } // Only successful request return Array return $json; } /** * Extract msg from json * @since 5.0 */ public function extract_msg( $json, $service, $server = false, $is_callback = false ) { if ( ! empty( $json[ '_info' ] ) ) { self::debug( '_info: ' . $json[ '_info' ] ); $msg = __( 'Message from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json[ '_info' ]; $msg .= $this->_parse_link( $json ); Admin_Display::info( $msg ); unset( $json[ '_info' ] ); } if ( ! empty( $json[ '_note' ] ) ) { self::debug( '_note: ' . $json[ '_note' ] ); $msg = __( 'Message from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json[ '_note' ]; $msg .= $this->_parse_link( $json ); Admin_Display::note( $msg ); unset( $json[ '_note' ] ); } if ( ! empty( $json[ '_success' ] ) ) { self::debug( '_success: ' . $json[ '_success' ] ); $msg = __( 'Good news from QUIC.cloud server', 'litespeed-cache' ) . ': ' . $json[ '_success' ]; $msg .= $this->_parse_link( $json ); Admin_Display::succeed( $msg ); unset( $json[ '_success' ] ); } // Upgrade is required if ( ! empty( $json[ '_err_req_v' ] ) ) { self::debug( '_err_req_v: ' . $json[ '_err_req_v' ] ); $msg = sprintf( __( '%1$s plugin version %2$s required for this action.', 'litespeed-cache' ), Core::NAME, 'v' . $json[ '_err_req_v' ] . '+' ) . " [server] $server [service] $service"; // Append upgrade link $msg2 = ' ' . GUI::plugin_upgrade_link( Core::NAME, Core::PLUGIN_NAME, $json[ '_err_req_v' ] ); $msg2 .= $this->_parse_link( $json ); Admin_Display::error( $msg . $msg2 ); return array( $json, true ); } // Parse _carry_on info if ( ! empty( $json[ '_carry_on' ] ) ) { self::debug( 'Carry_on usage', $json[ '_carry_on' ] ); // Store generic info foreach ( array( 'usage', 'promo', '_err', '_info', '_note', '_success' ) as $v ) { if ( ! empty( $json[ '_carry_on' ][ $v ] ) ) { switch ( $v ) { case 'usage': $usage_svc_tag = in_array( $service, array( self::SVC_CCSS, self::SVC_UCSS, self::SVC_VPI ) ) ? self::SVC_PAGE_OPTM : $service; $this->_summary[ 'usage.' . $usage_svc_tag ] = $json[ '_carry_on' ][ $v ]; break; case 'promo': if ( empty( $this->_summary[ $v ] ) || ! is_array( $this->_summary[ $v ] ) ) { $this->_summary[ $v ] = array(); } $this->_summary[ $v ][] = $json[ '_carry_on' ][ $v ]; break; case '_error': case '_info': case '_note': case '_success': $color_mode = substr( $v, 1 ); $msgs = $json[ '_carry_on' ][ $v ]; Admin_Display::add_unique_notice( $color_mode, $msgs, true ); break; default: break; } } } self::save_summary(); unset( $json[ '_carry_on' ] ); } // Parse general error msg if ( !$is_callback && ( empty( $json[ '_res' ] ) || $json[ '_res' ] !== 'ok' ) ) { $json_msg = ! empty( $json[ '_msg' ] ) ? $json[ '_msg' ] : 'unknown'; self::debug( '❌ _err: ' . $json_msg, $json ); $str_translated = Error::msg( $json_msg ); $msg = __( 'Failed to communicate with QUIC.cloud server', 'litespeed-cache' ) . ': ' . $str_translated . " [server] $server [service] $service"; $msg .= $this->_parse_link( $json ); Admin_Display::error( $msg ); // QC may try auto alias /** @since 5.0 Store the domain as `err_domains` only for QC auto alias feature */ if ( $json_msg == 'err_alias' ) { if ( empty( $this->_summary[ 'err_domains' ] ) ) { $this->_summary[ 'err_domains' ] = array(); } $home_url = home_url(); if ( ! array_key_exists( $home_url, $this->_summary[ 'err_domains' ] ) ) { $this->_summary[ 'err_domains' ][ $home_url ] = time(); } self::save_summary(); } // Site not on QC, delete invalid domain key if ( $json_msg == 'site_not_registered' || $json_msg == 'err_key' ) { $this->_clean_api_key(); } return array( $json, true ); } unset( $json[ '_res' ] ); if ( ! empty( $json[ '_msg' ] ) ) { unset( $json[ '_msg' ] ); } return array( $json, false ); } /** * Clear API key and QC linked status * @since 5.0 */ private function _clean_api_key() { $this->cls( 'Conf' )->update_confs( array( self::O_API_KEY => '' ) ); $this->_summary['is_linked'] = 0; self::save_summary(); $msg = __( 'Site not recognized. Domain Key has been automatically removed. Please request a new one.', 'litespeed-cache' ); $msg .= Doc::learn_more( admin_url( 'admin.php?page=litespeed-general' ), __( 'Click here to set.', 'litespeed-cache' ), true, false, true ); $msg .= Doc::learn_more( 'https://docs.litespeedtech.com/lscache/lscwp/general/#domain-key', false, false, false, true ); Admin_Display::error( $msg, false, true ); } /** * REST call: check if the error domain is valid call for auto alias purpose * @since 5.0 */ public function rest_err_domains() { // Validate token hash first if ( empty( $_POST[ 'hash' ] ) || empty( $_POST[ 'main_domain' ] ) || empty( $_POST[ 'alias' ] ) ) { return self::err( 'lack_of_param' ); } if ( ! $this->_api_key || $_POST[ 'hash' ] !== md5( substr( $this->_api_key, 1, 8 ) ) ) { return self::err( 'wrong_hash' ); } list( $post_data ) = $this->extract_msg( $_POST, 'Quic.cloud', false, true ); if ( $this->_is_err_domain( $_POST[ 'alias' ] ) ) { $this->_remove_domain_from_err_list( $_POST[ 'alias' ] ); $res_hash = substr( $this->_api_key, 2, 4 ); self::debug( '__callback IP request hash: md5(' . $res_hash . ')' ); return self::ok( array( 'hash' => md5( $res_hash ) ) ); } return self::err( 'Not an alias req from here' ); } /** * Remove a domain from err domain * @since 5.0 */ private function _remove_domain_from_err_list( $url ) { unset( $this->_summary[ 'err_domains' ][ $url ] ); self::save_summary(); } /** * Check if is err domain * @since 5.0 */ private function _is_err_domain( $home_url ) { if ( empty( $this->_summary[ 'err_domains' ] ) ) return false; if ( ! array_key_exists( $home_url, $this->_summary[ 'err_domains' ] ) ) return false; // Auto delete if too long ago if ( time() - $this->_summary[ 'err_domains' ][ $home_url ] > 86400 * 10 ) { $this->_remove_domain_from_err_list( $home_url ); } if ( time() - $this->_summary[ 'err_domains' ][ $home_url ] > 86400 ) return false; return true; } public function req_rest_api($api, $body = array()) { $token = $this->_setup_token; if (empty($token)) { Admin_Display::error( __( 'Cannot request REST API, no token saved.', 'litespeed-cache' )); return; } $req_args = array( 'headers' => array( 'Authorization' => 'bearer ' . $token, 'Content-Type' => 'application/json', ), ); if (!empty($body)) { $req_args['body'] = json_encode($body); $response = wp_remote_post(self::CLOUD_SERVER . '/v2' . $api, $req_args); } else { $response = wp_remote_get(self::CLOUD_SERVER . '/v2' . $api, $req_args); } return $this->_parse_rest_response($response); } private function _parse_rest_response($response) { if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to request REST API: ' . $error_message ); Admin_Display::error( __( 'Cloud REST Error', 'litespeed-cache' ) . ': ' . $error_message ); return $error_message; } $json = json_decode( $response[ 'body' ], true ); if (!$json['success']) { if (isset($json['info']['errors'])) { $errs = array(); foreach ($json['info']['errors'] as $err) { $errs[] = 'Error ' . $err['code'] . ': ' . $err['message']; } $error_message = implode('<br>', $errs); } else { $error_message = 'Unknown error, contact QUIC.cloud support.'; } Admin_Display::error( __( 'Cloud REST API returned error: ', 'litespeed-cache' ) . $error_message ); return $error_message; } return $json; } /** * Show promo from cloud * * @since 3.0 * @access public */ public function show_promo() { // if ( ! $this->_api_key && ! defined( 'LITESPEED_DISMISS_DOMAIN_KEY' ) ) { // Admin_Display::error( Error::msg( 'lack_of_api_key' ), true ); // } if ( empty( $this->_summary[ 'promo' ] ) ) { return; } require_once LSCWP_DIR . 'tpl/banner/cloud_promo.tpl.php' ; } /** * Clear promo from cloud * * @since 3.0 * @access private */ private function _clear_promo() { if ( count( $this->_summary[ 'promo' ] ) > 1 ) { array_shift( $this->_summary[ 'promo' ] ); } else { $this->_summary[ 'promo' ] = array(); } self::save_summary(); } /** * Parse _links from json * * @since 1.6.5 * @since 1.6.7 Self clean the parameter * @access private */ private function _parse_link( &$json ) { $msg = ''; if ( ! empty( $json[ '_links' ] ) ) { foreach ( $json[ '_links' ] as $v ) { $msg .= ' ' . sprintf( '<a href="%s" class="%s" target="_blank">%s</a>', $v[ 'link' ], ! empty( $v[ 'cls' ] ) ? $v[ 'cls' ] : '', $v[ 'title' ] ); } unset( $json[ '_links' ] ); } return $msg; } /** * Request callback validation from Cloud * * @since 3.0 * @access public */ public function ip_validate() { if ( empty( $_POST[ 'hash' ] ) ) { self::debug( 'Lack of hash param' ); return self::err( 'lack_of_param' ); } if ( empty( $this->_api_key ) ) { self::debug( 'Lack of API key' ); return self::err( 'lack_of_api_key' ); } $to_validate = substr( $this->_api_key, 0, 4 ); if ( $_POST[ 'hash' ] !== md5( $to_validate ) ) { self::debug( '__callback IP request hash wrong: md5(' . $to_validate . ') !== ' . $_POST[ 'hash' ] ); return self::err( 'err_hash' ); } Control::set_nocache( 'Cloud IP hash validation' ); $res_hash = substr( $this->_api_key, 2, 4 ); self::debug( '__callback IP request hash: md5(' . $res_hash . ')' ); return self::ok( array( 'hash' => md5( $res_hash ) ) ); } /** * Can apply for a new token or not * * @since 3.0 */ public function can_token() { return empty( $this->_summary[ 'token_ts' ] ) || time() - $this->_summary[ 'token_ts' ] > self::EXPIRATION_TOKEN; } public function set_keygen_token($token) { $this->_summary[ 'token' ] = $token; $this->_summary[ 'token_ts' ] = time(); if ( ! empty( $this->_summary[ 'apikey_ts' ] ) ) { unset( $this->_summary[ 'apikey_ts' ] ); } self::save_summary(); } /** * Send request for domain key, get json [ 'token' => 'asdfasdf' ] * * @since 3.0 * @access public */ public function gen_key() { $data = array( 'site_url' => home_url(), 'rest' => function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : apply_filters( 'rest_url_prefix', 'wp-json' ), 'server_ip' => $this->conf( self::O_SERVER_IP ), ); if ( ! empty( $this->_summary[ 'token' ] ) ) { $data[ 'token' ] = $this->_summary[ 'token' ]; } $response = wp_remote_get( self::CLOUD_SERVER . '/d/req_key?data=' . Utility::arr2str( $data ) ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to gen_key: ' . $error_message ); Admin_Display::error( __( 'Cloud Error', 'litespeed-cache' ) . ': ' . $error_message ); return; } $json = json_decode( $response[ 'body' ], true ); // Save token option if ( ! empty( $json[ 'token' ] ) ) { $this->set_keygen_token( $json[ 'token' ] ); } // Parse general error msg if ( empty( $json[ '_res' ] ) || $json[ '_res' ] !== 'ok' ) { // clear current token unset( $this->_summary[ 'token' ] ); self::save_summary(); $json_msg = ! empty( $json[ '_msg' ] ) ? $json[ '_msg' ] : 'unknown'; self::debug( '❌ _err: ' . $json_msg ); $msg = __( 'Failed to communicate with QUIC.cloud server', 'litespeed-cache' ) . ': ' . Error::msg( $json_msg ); $msg .= $this->_parse_link( $json ); Admin_Display::error( $msg ); return; } // This is a ok msg if ( ! empty( $json[ '_msg' ] ) ) { self::debug( '_msg: ' . $json[ '_msg' ] ); $msg = __( 'Message from QUIC.cloud server', 'litespeed-cache' ) . ': ' . Error::msg( $json[ '_msg' ] ); $msg .= $this->_parse_link( $json ); Admin_Display::info( $msg ); return; } self::debug( '✅ send request for key successfully.' ); Admin_Display::succeed( __( 'Applied for Domain Key successfully. Please wait for result. Domain Key will be automatically sent to your WordPress.', 'litespeed-cache' ) ); } /** * Token callback validation from Cloud * * @since 3.0 * @access public */ public function token_validate() { try { $this->_validate_hash(); } catch( \Exception $e ) { return self::err( $e->getMessage() ); } Control::set_nocache( 'Cloud token validation' ); self::debug( '✅ __callback token validation passed' ); return self::ok( array( 'hash' => md5( substr( $this->_summary[ 'token' ], 3, 8 ) ) ) ); } /** * Callback for approval of api key after validated token and gen key from QUIC.cloud * * @since 3.0 * @access public */ public function save_apikey() { // Validate token hash first if ( empty( $_POST[ 'domain_key' ] ) || ! isset( $_POST[ 'is_linked' ] ) ) { return self::err( 'lack_of_param' ); } try { $this->_validate_hash( 1 ); } catch( \Exception $e ) { return self::err( $e->getMessage() ); } // This doesn't need to sync QUIC conf but need to clear nodes $this->cls( 'Conf' )->update_confs( array( self::O_API_KEY => $_POST[ 'domain_key' ] ) ); $this->_summary[ 'is_linked' ] = $_POST[ 'is_linked' ] ? 1 : 0; $this->_summary[ 'apikey_ts' ] = time(); if ( ! empty( $_POST[ 'main_domain' ] ) ) { $this->_summary[ 'main_domain' ] = $_POST[ 'main_domain' ]; } // Clear token unset( $this->_summary[ 'token' ] ); self::save_summary(); self::debug( '✅ saved auth_key' ); Admin_Display::succeed( '🎊 ' . __( 'Congratulations, your Domain Key has been approved! The setting has been updated accordingly.', 'litespeed-cache' ) ); return self::ok(); } /** * Validate POST hash match local token or not * * @since 3.0 */ private function _validate_hash( $offset = 0 ) { if ( empty( $_POST[ 'hash' ] ) ) { self::debug( 'Lack of hash param' ); throw new \Exception( 'lack_of_param' ); } if ( empty( $this->_summary[ 'token' ] ) ) { self::debug( 'token validate failed: token not exist' ); throw new \Exception( 'lack_of_local_token' ); } if ( $_POST[ 'hash' ] !== md5( substr( $this->_summary[ 'token' ], $offset, 8 ) ) ) { self::debug( 'token validate failed: token mismatch hash !== ' . $_POST[ 'hash' ] ); throw new \Exception( 'mismatch' ); } } /** * If can link the domain to QC user or not * * @since 3.0 */ public function can_link_qc() { return empty( $this->_summary[ 'is_linked' ] ) && $this->_api_key; } /** * Link the domain to QC user * * @since 3.0 */ private function _link_to_qc() { if ( ! $this->can_link_qc() ) { return; } $data = array( 'site_url' => home_url(), 'domain_hash' => md5( substr( $this->_api_key, 0, 8 ) ), 'ref' => get_admin_url( null, 'admin.php?page=litespeed-general' ), ); wp_redirect( self::CLOUD_SERVER_DASH . '/u/wp?data=' . Utility::arr2str( $data ) ); exit; } public function set_linked() { $this->_summary[ 'is_linked' ] = 1; self::save_summary(); } /** * Update is_linked status if is a redirected back from QC * * @since 3.0 * @since 5.0 renamed update_is_linked_status -> parse_qc_redir, add param for additional args. Return args if exist. */ public function parse_qc_redir($extra = array()) { $extraRet = array(); $qsDrop = array(); if ( ! $this->_api_key && ! empty( $this->_summary[ 'is_linked' ] ) ) { $this->_summary[ 'is_linked' ] = 0; self::save_summary(); } if ( empty( $_GET[ 'qc_res' ] ) ) { return $extraRet; } $qsDrop[] = ".replace( '&qc_res=" . sanitize_key( $_GET[ 'qc_res' ] ) . ', \'\' )'; if ( ! empty( $_GET[ 'domain_hash' ] ) ) { if ( md5( substr( $this->_api_key, 2, 8 ) ) !== $_GET[ 'domain_hash' ] ) { Admin_Display::error( __( 'Domain Key hash mismatch', 'litespeed-cache' ), true ); return $extraRet; } $this->set_linked(); $qsDrop[] = ".replace( '&domain_hash=" . sanitize_key( $_GET[ 'domain_hash' ] ) . ', \'\' )'; } if ( ! empty( $extra ) ) { foreach ( $extra as $key ) { if ( ! empty( $_GET[ $key ] ) ) { $extraRet[ $key ] = $_GET[ $key ]; $qsDrop[] = ".replace( '&$key=" . urlencode( $_GET[ $key ] ) . ', \'\' )'; } } } $replaceStr = implode('', $qsDrop); // Drop QS echo "<script>window.history.pushState( 'remove_gen_link', document.title, window.location.href" . $replaceStr . " );</script>"; return $extraRet; } /** * Check if this visit is from cloud or not * * @since 3.0 */ public function is_from_cloud() { if ( empty( $this->_summary[ 'ips' ] ) || empty( $this->_summary[ 'ips_ts' ] ) || time() - $this->_summary[ 'ips_ts' ] > 86400 * self::TTL_IPS ) { $this->_update_ips(); } $res = $this->cls( 'Router' )->ip_access( $this->_summary[ 'ips' ] ); if ( ! $res ) { self::debug( '❌ Not our cloud IP' ); // Refresh IP list for future detection $this->_update_ips(); } else { self::debug( '✅ Passed Cloud IP verification' ); } return $res; } /** * Update Cloud IP list * * @since 4.2 */ private function _update_ips() { self::debug( 'Load remote Cloud IP list from ' . self::CLOUD_IPS ); $response = wp_remote_get( self::CLOUD_IPS . '?json' ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); self::debug( 'failed to get ip whitelist: ' . $error_message ); throw new \Exception( 'Failed to fetch QUIC.cloud whitelist ' . $error_message ); } $json = json_decode( $response[ 'body' ], true ); self::save_summary( array( 'ips_ts' => time(), 'ips' => $json ) ); } /** * Return succeeded response * * @since 3.0 */ public static function ok( $data = array() ) { $data[ '_res' ] = 'ok'; return $data; } /** * Return error * * @since 3.0 */ public static function err( $code ) { return array( '_res' => 'err', '_msg' => $code ); } /** * Handle all request actions from main cls * * @since 3.0 * @access public */ public function handler() { $type = Router::verify_type(); switch ( $type ) { case self::TYPE_CLEAR_CLOUD: $this->clear_cloud(); break; case self::TYPE_REDETECT_CLOUD: if ( ! empty( $_GET[ 'svc' ] ) ) { $this->detect_cloud( $_GET[ 'svc' ], true ); } break; case self::TYPE_CLEAR_PROMO: $this->_clear_promo(); break; case self::TYPE_GEN_KEY: $this->gen_key(); break; case self::TYPE_LINK: $this->_link_to_qc(); break; case self::TYPE_SYNC_USAGE: $this->sync_usage(); $msg = __( 'Sync credit allowance with Cloud Server successfully.', 'litespeed-cache' ) ; Admin_Display::succeed( $msg ) ; break; default: break; } Admin::redirect(); } }