Source: Providers/OpenAI/APIRequest.php

<?php

namespace Classifai\Providers\OpenAI;

use WP_Error;

/**
 * The APIRequest class is a low level class to make OpenAI API
 * requests.
 *
 * The returned response is parsed into JSON and returned as an
 * associative array.
 *
 * Usage:
 *
 * $request = new Classifai\Providers\OpenAI\APIRequest();
 * $request->post( $openai_url, $options );
 */
class APIRequest {

	/**
	 * The OpenAI API key.
	 *
	 * @var string
	 */
	public $api_key;

	/**
	 * The feature name.
	 *
	 * @var string
	 */
	public $feature;

	/**
	 * OpenAI APIRequest constructor.
	 *
	 * @param string $api_key OpenAI API key.
	 * @param string $feature Feature name.
	 */
	public function __construct( string $api_key = '', string $feature = '' ) {
		$this->api_key = $api_key;
		$this->feature = $feature;
	}

	/**
	 * Makes an authorized GET request.
	 *
	 * @param string $url The OpenAI API url
	 * @param array  $options Additional query params
	 * @return array|WP_Error
	 */
	public function get( string $url, array $options = [] ) {
		/**
		 * Filter the URL for the get request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_get_url
		 *
		 * @param {string} $url The URL for the request.
		 * @param {array} $options The options for the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {string} The URL for the request.
		 */
		$url = apply_filters( 'classifai_openai_api_request_get_url', $url, $options, $this->feature );

		/**
		 * Filter the options for the get request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_get_options
		 *
		 * @param {array} $options The options for the request.
		 * @param {string} $url The URL for the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {array} The options for the request.
		 */
		$options = apply_filters( 'classifai_openai_api_request_get_options', $options, $url, $this->feature );

		$this->add_headers( $options );

		/**
		 * Filter the response from OpenAI for a get request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_response_get
		 *
		 * @param {string} $url Request URL.
		 * @param {array} $options Request body options.
		 * @param {string} $this->feature Feature name.
		 *
		 * @return {array} API response.
		 */
		return apply_filters(
			'classifai_openai_api_response_get',
			$this->get_result( wp_remote_get( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
			$url,
			$options,
			$this->feature
		);
	}

	/**
	 * Makes an authorized POST request.
	 *
	 * @param string $url The OpenAI API URL.
	 * @param array  $options Additional query params.
	 * @return array|WP_Error
	 */
	public function post( string $url = '', array $options = [] ) {
		$options = wp_parse_args(
			$options,
			[
				'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
			]
		);

		/**
		 * Filter the URL for the post request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_post_url
		 *
		 * @param {string} $url The URL for the request.
		 * @param {array} $options The options for the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {string} The URL for the request.
		 */
		$url = apply_filters( 'classifai_openai_api_request_post_url', $url, $options, $this->feature );

		/**
		 * Filter the options for the post request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_post_options
		 *
		 * @param {array} $options The options for the request.
		 * @param {string} $url The URL for the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {array} The options for the request.
		 */
		$options = apply_filters( 'classifai_openai_api_request_post_options', $options, $url, $this->feature );

		$this->add_headers( $options );

		/**
		 * Filter the response from OpenAI for a post request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_response_post
		 *
		 * @param {string} $url Request URL.
		 * @param {array} $options Request body options.
		 * @param {string} $this->feature Feature name.
		 *
		 * @return {array} API response.
		 */
		return apply_filters(
			'classifai_openai_api_response_post',
			$this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
			$url,
			$options,
			$this->feature
		);
	}

	/**
	 * Makes an authorized POST request with form data.
	 *
	 * @param string $url The OpenAI API URL.
	 * @param array  $body The body of the request.
	 * @return array|WP_Error
	 */
	public function post_form( string $url = '', array $body = [] ) {
		/**
		 * Filter the URL for the post form request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_post_form_url
		 *
		 * @param {string} $url The URL for the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {string} The URL for the request.
		 */
		$url = apply_filters( 'classifai_openai_api_request_post_form_url', $url, $this->feature );

		$boundary = wp_generate_password( 24, false );
		$payload  = '';

		// Take all our POST fields and transform them to work with form-data.
		foreach ( $body as $name => $value ) {
			$payload .= '--' . $boundary;
			$payload .= "\r\n";

			if ( 'file' === $name ) {
				$payload .= 'Content-Disposition: form-data; name="file"; filename="' . basename( $value ) . '"' . "\r\n";
				$payload .= "\r\n";
				$payload .= file_get_contents( $value ); // phpcs:ignore
			} else {
				$payload .= 'Content-Disposition: form-data; name="' . esc_attr( $name ) .
					'"' . "\r\n\r\n";
				$payload .= esc_attr( $value );
			}

			$payload .= "\r\n";
		}

		$payload .= '--' . $boundary . '--';

		/**
		 * Filter the options for the post form request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_request_post_form_options
		 *
		 * @param {array} $options The options for the request.
		 * @param {string} $url The URL for the request.
		 * @param {array} $body The body of the request.
		 * @param {string} $this->feature The feature name.
		 *
		 * @return {array} The options for the request.
		 */
		$options = apply_filters(
			'classifai_openai_api_request_post_form_options',
			[
				'body'    => $payload,
				'headers' => [
					'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
				],
				'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
			],
			$url,
			$body,
			$this->feature
		);

		$this->add_headers( $options );

		/**
		 * Filter the response from OpenAI for a post form request.
		 *
		 * @since 2.4.0
		 * @hook classifai_openai_api_response_post_form
		 *
		 * @param {string} $url Request URL.
		 * @param {array} $options Request body options.
		 * @param {string} $this->feature Feature name.
		 *
		 * @return {array} API response.
		 */
		return apply_filters(
			'classifai_openai_api_response_post_form',
			$this->get_result( wp_remote_post( $url, $options ) ), // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.wp_remote_get_wp_remote_get
			$url,
			$options,
			$this->feature
		);
	}

	/**
	 * Get results from the response.
	 *
	 * @param object $response The API response.
	 * @return array|WP_Error
	 */
	public function get_result( $response ) {
		if ( ! is_wp_error( $response ) ) {
			$headers      = wp_remote_retrieve_headers( $response );
			$content_type = false;

			if ( ! is_wp_error( $headers ) ) {
				$content_type = isset( $headers['content-type'] ) ? $headers['content-type'] : false;
			}

			$body = wp_remote_retrieve_body( $response );
			$code = wp_remote_retrieve_response_code( $response );

			if ( false === $content_type || false !== strpos( $content_type, 'application/json' ) ) {
				$json = json_decode( $body, true );

				if ( json_last_error() === JSON_ERROR_NONE ) {
					if ( empty( $json['error'] ) ) {
						return $json;
					} else {
						$message = $json['error']['message'] ?? esc_html__( 'An error occured', 'classifai' );
						return new WP_Error( $code, $message );
					}
				} else {
					return new WP_Error( 'Invalid JSON: ' . json_last_error_msg(), $body );
				}
			} elseif ( $content_type && false !== strpos( $content_type, 'audio/mpeg' ) ) {
				return $response;
			} else {
				return new WP_Error( 'Invalid content type', $response );
			}
		} else {
			return $response;
		}
	}

	/**
	 * Add the headers.
	 *
	 * @param array $options The header options, passed by reference.
	 */
	public function add_headers( array &$options = [] ) {
		if ( empty( $options['headers'] ) ) {
			$options['headers'] = [];
		}

		if ( ! isset( $options['headers']['Authorization'] ) ) {
			$options['headers']['Authorization'] = $this->get_auth_header();
		}

		if ( ! isset( $options['headers']['Content-Type'] ) ) {
			$options['headers']['Content-Type'] = 'application/json';
		}
	}

	/**
	 * Get the auth header.
	 *
	 * @return string
	 */
	public function get_auth_header() {
		return 'Bearer ' . $this->get_api_key();
	}

	/**
	 * Get the OpenAI API key.
	 *
	 * @return string
	 */
	public function get_api_key() {
		return $this->api_key;
	}
}