settings ) { $this->settings = AIPMSP_Proxy::get_settings(); } return $this->settings; } /** * Проверяет, нужно ли проксировать запрос к указанному URL. * * @since 1.0.0 * * @param string $url URL запроса. * @return bool */ private function should_proxy( $url ) { $settings = $this->settings(); if ( empty( $settings['proxy_enabled'] ) ) { return false; } $host = wp_parse_url( $url, PHP_URL_HOST ); if ( empty( $host ) ) { return false; } $host = strtolower( $host ); foreach ( $settings['domains'] as $domain ) { $domain = strtolower( trim( $domain ) ); if ( '' === $domain ) { continue; } // Точное совпадение или поддомен разрешённого домена. if ( $host === $domain || self::str_ends_with( $host, '.' . $domain ) ) { return true; } } return false; } /** * Хук http_api_curl: подставляет настройки прокси в cURL-хендл. * * @since 1.0.0 * * @param resource $handle cURL-хендл. * @param array $args Аргументы запроса. * @param string $url URL запроса. * @return void */ public function apply_proxy( $handle, $args, $url ) { if ( ! $this->should_proxy( $url ) ) { return; } $enabled = AIPMSP_Proxy::get_enabled_proxies( $this->settings() ); if ( empty( $enabled ) ) { return; } // Ротация: выбираем прокси по текущему индексу (с цикличностью). $count = count( $enabled ); $index = $this->rotation_index % $count; $proxy = $enabled[ $index ]; $pass = isset( $proxy['pass'] ) ? AIPMSP_Crypto::decrypt( $proxy['pass'] ) : ''; curl_setopt( $handle, CURLOPT_PROXY, $proxy['ip'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions curl_setopt( $handle, CURLOPT_PROXYPORT, (int) $proxy['port'] ); // phpcs:ignore WordPress.WP.AlternativeFunctions curl_setopt( $handle, CURLOPT_PROXYTYPE, AIPMSP_Proxy::curl_proxy_type( $proxy['type'] ) ); // phpcs:ignore WordPress.WP.AlternativeFunctions if ( ! empty( $proxy['user'] ) && '' !== $pass ) { curl_setopt( $handle, CURLOPT_PROXYUSERPWD, $proxy['user'] . ':' . $pass ); // phpcs:ignore WordPress.WP.AlternativeFunctions } curl_setopt( $handle, CURLOPT_CONNECTTIMEOUT, 10 ); // phpcs:ignore WordPress.WP.AlternativeFunctions } /** * Хук http_response: при ошибке соединения пробует следующий прокси. * * @since 1.0.0 * * @param array $response Ответ HTTP API. * @param array $parsed_args Аргументы запроса. * @param string $url URL запроса. * @return array */ public function maybe_rotate( $response, $parsed_args, $url ) { if ( ! $this->should_proxy( $url ) ) { return $response; } $enabled = AIPMSP_Proxy::get_enabled_proxies( $this->settings() ); $count = count( $enabled ); if ( $count < 2 ) { return $response; } // Считаем неудачей ошибку транспорта или 5xx/0 коды. $is_error = is_wp_error( $response ); $code = $is_error ? 0 : (int) wp_remote_retrieve_response_code( $response ); $failed = $is_error || 0 === $code || $code >= 500; // Ограничиваем число попыток числом доступных прокси. $attempts = isset( $parsed_args['_aipmsp_attempts'] ) ? (int) $parsed_args['_aipmsp_attempts'] : 0; if ( $failed && $attempts < $count - 1 ) { $this->rotation_index++; $retry_args = $parsed_args; $retry_args['_aipmsp_attempts'] = $attempts + 1; // Индекс ротации уже сдвинут, повторный запрос пойдёт через следующий прокси. $retry = wp_remote_request( $url, $retry_args ); return $retry; } return $response; } /** * Полифилл str_ends_with для PHP < 8.0. * * @since 1.0.0 * * @param string $haystack Строка. * @param string $needle Окончание. * @return bool */ private static function str_ends_with( $haystack, $needle ) { if ( function_exists( 'str_ends_with' ) ) { return str_ends_with( $haystack, $needle ); } $len = strlen( $needle ); if ( 0 === $len ) { return true; } return substr( $haystack, -$len ) === $needle; } }