-%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  ¥2" ÿÄ               ÿÄ J  	     ! 1AQ"aq2‘#BR‚¡ÁÑ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ              ÿÄ *        !1AQa‘"2q3±ð#b¡ÿÚ   ? ¼QxJQaÍuò¸Zö Úü8,ÐÚú
"SSn<rçù–´âE—^ªBÖ9À\†¸ÔÁT­ÃÛ5
ëd´³Í#Ý;Þ38œî ¶H£M:wÎ3…³…âpÔF&‚FK¸9„â4àGEõªfÿ ‘ñ(ßw­pŽF|È¥ù®häðÍÑ¶¹‘[ÒinÙW¶ùñY˜Q{›K"išÒ[Ú8žë\F¹@-?v"ÔU”,ìöžkÿ {I‡£šÍ?e
ríV
..............................................................................................................................................................................
.............................................................................                                                  
                                                                                                                                                                                     ÿØÿà JFIF      ÿÛ „ 	 ( %!1!%)+//.383,7(-.+



-%%-////---/-.+/--+------/------/--0+--/-/-----.-----ÿÀ  ¥2" ÿÄ               ÿÄ J  	     ! 1AQ"aq2‘#BR‚¡ÁÑ3br’¢±Âð$CSƒ²á4c“%DsÓñÿÄ              ÿÄ *        !1AQa‘"2q3±ð#b¡ÿÚ   ? ¼QxJQaÍuò¸Zö Úü8,ÐÚú
"SSn<rçù–´âE—^ªBÖ9À\†¸ÔÁT­ÃÛ5
ëd´³Í#Ý;Þ38œî ¶H£M:wÎ3…³…âpÔF&‚FK¸9„â4àGEõªfÿ ‘ñ(ßw­pŽF|È¥ù®häðÍÑ¶¹‘[ÒinÙW¶ùñY˜Q{›K"išÒ[Ú8žë\F¹@-?v"ÔU”,ìöžkÿ {I‡£šÍ?e
ríV
..............................................................................................................................................................................
.............................................................................                                                  
                                                                                                                                                                                     Capability.php                                                                                      0000644                 00000001214 15227642210 0007335 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Capability interface declaring the known capabilities.
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests;

/**
 * Capability interface declaring the known capabilities.
 *
 * This is used as the authoritative source for which capabilities can be queried.
 *
 * @package Requests\Utilities
 */
interface Capability {

	/**
	 * Support for SSL.
	 *
	 * @var string
	 */
	const SSL = 'ssl';

	/**
	 * Collection of all capabilities supported in Requests.
	 *
	 * Note: this does not automatically mean that the capability will be supported for your chosen transport!
	 *
	 * @var string[]
	 */
	const ALL = [
		self::SSL,
	];
}
                                                                                                                                                                                                                                                                                                                                                                                    Hooks.php                                                                                           0000644                 00000005730 15227642210 0006346 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Handles adding and dispatching events
 *
 * @package Requests\EventDispatcher
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\HookManager;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Handles adding and dispatching events
 *
 * @package Requests\EventDispatcher
 */
class Hooks implements HookManager {
	/**
	 * Registered callbacks for each hook
	 *
	 * @var array
	 */
	protected $hooks = [];

	/**
	 * Register a callback for a hook
	 *
	 * @param string $hook Hook name
	 * @param callable $callback Function/method to call on event
	 * @param int $priority Priority number. <0 is executed earlier, >0 is executed later
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $callback argument is not callable.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $priority argument is not an integer.
	 */
	public function register($hook, $callback, $priority = 0) {
		if (is_string($hook) === false) {
			throw InvalidArgument::create(1, '$hook', 'string', gettype($hook));
		}

		if (is_callable($callback) === false) {
			throw InvalidArgument::create(2, '$callback', 'callable', gettype($callback));
		}

		if (InputValidator::is_numeric_array_key($priority) === false) {
			throw InvalidArgument::create(3, '$priority', 'integer', gettype($priority));
		}

		if (!isset($this->hooks[$hook])) {
			$this->hooks[$hook] = [
				$priority => [],
			];
		} elseif (!isset($this->hooks[$hook][$priority])) {
			$this->hooks[$hook][$priority] = [];
		}

		$this->hooks[$hook][$priority][] = $callback;
	}

	/**
	 * Dispatch a message
	 *
	 * @param string $hook Hook name
	 * @param array $parameters Parameters to pass to callbacks
	 * @return boolean Successfulness
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $hook argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $parameters argument is not an array.
	 */
	public function dispatch($hook, $parameters = []) {
		if (is_string($hook) === false) {
			throw InvalidArgument::create(1, '$hook', 'string', gettype($hook));
		}

		// Check strictly against array, as Array* objects don't work in combination with `call_user_func_array()`.
		if (is_array($parameters) === false) {
			throw InvalidArgument::create(2, '$parameters', 'array', gettype($parameters));
		}

		if (empty($this->hooks[$hook])) {
			return false;
		}

		if (!empty($parameters)) {
			// Strip potential keys from the array to prevent them being interpreted as parameter names in PHP 8.0.
			$parameters = array_values($parameters);
		}

		ksort($this->hooks[$hook]);

		foreach ($this->hooks[$hook] as $priority => $hooked) {
			foreach ($hooked as $callback) {
				$callback(...$parameters);
			}
		}

		return true;
	}

	public function __wakeup() {
		throw new \LogicException( __CLASS__ . ' should never be unserialized' );
	}
}
                                        Proxy.php                                                                                           0000644                 00000001543 15227642210 0006402 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Proxy connection interface
 *
 * @package Requests\Proxy
 * @since   1.6
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Hooks;

/**
 * Proxy connection interface
 *
 * Implement this interface to handle proxy settings and authentication
 *
 * Parameters should be passed via the constructor where possible, as this
 * makes it much easier for users to use your provider.
 *
 * @see \WpOrg\Requests\Hooks
 *
 * @package Requests\Proxy
 * @since   1.6
 */
interface Proxy {
	/**
	 * Register hooks as needed
	 *
	 * This method is called in {@see \WpOrg\Requests\Requests::request()} when the user
	 * has set an instance as the 'auth' option. Use this callback to register all the
	 * hooks you'll need.
	 *
	 * @see \WpOrg\Requests\Hooks::register()
	 * @param \WpOrg\Requests\Hooks $hooks Hook system
	 */
	public function register(Hooks $hooks);
}
                                                                                                                                                             Auth.php                                                                                            0000644                 00000001534 15227642210 0006162 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Authentication provider interface
 *
 * @package Requests\Authentication
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Hooks;

/**
 * Authentication provider interface
 *
 * Implement this interface to act as an authentication provider.
 *
 * Parameters should be passed via the constructor where possible, as this
 * makes it much easier for users to use your provider.
 *
 * @see \WpOrg\Requests\Hooks
 *
 * @package Requests\Authentication
 */
interface Auth {
	/**
	 * Register hooks as needed
	 *
	 * This method is called in {@see \WpOrg\Requests\Requests::request()} when the user
	 * has set an instance as the 'auth' option. Use this callback to register all the
	 * hooks you'll need.
	 *
	 * @see \WpOrg\Requests\Hooks::register()
	 * @param \WpOrg\Requests\Hooks $hooks Hook system
	 */
	public function register(Hooks $hooks);
}
                                                                                                                                                                    Cookie.php                                                                                          0000644                 00000036035 15227642210 0006476 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Cookie storage object
 *
 * @package Requests\Cookies
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Response\Headers;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Cookie storage object
 *
 * @package Requests\Cookies
 */
class Cookie {
	/**
	 * Cookie name.
	 *
	 * @var string
	 */
	public $name;

	/**
	 * Cookie value.
	 *
	 * @var string
	 */
	public $value;

	/**
	 * Cookie attributes
	 *
	 * Valid keys are `'path'`, `'domain'`, `'expires'`, `'max-age'`, `'secure'` and
	 * `'httponly'`.
	 *
	 * @var \WpOrg\Requests\Utility\CaseInsensitiveDictionary|array Array-like object
	 */
	public $attributes = [];

	/**
	 * Cookie flags
	 *
	 * Valid keys are `'creation'`, `'last-access'`, `'persistent'` and `'host-only'`.
	 *
	 * @var array
	 */
	public $flags = [];

	/**
	 * Reference time for relative calculations
	 *
	 * This is used in place of `time()` when calculating Max-Age expiration and
	 * checking time validity.
	 *
	 * @var int
	 */
	public $reference_time = 0;

	/**
	 * Create a new cookie object
	 *
	 * @param string                                                  $name           The name of the cookie.
	 * @param string                                                  $value          The value for the cookie.
	 * @param array|\WpOrg\Requests\Utility\CaseInsensitiveDictionary $attributes Associative array of attribute data
	 * @param array                                                   $flags          The flags for the cookie.
	 *                                                                                Valid keys are `'creation'`, `'last-access'`,
	 *                                                                                `'persistent'` and `'host-only'`.
	 * @param int|null                                                $reference_time Reference time for relative calculations.
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $value argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $attributes argument is not an array or iterable object with array access.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $flags argument is not an array.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $reference_time argument is not an integer or null.
	 */
	public function __construct($name, $value, $attributes = [], $flags = [], $reference_time = null) {
		if (is_string($name) === false) {
			throw InvalidArgument::create(1, '$name', 'string', gettype($name));
		}

		if (is_string($value) === false) {
			throw InvalidArgument::create(2, '$value', 'string', gettype($value));
		}

		if (InputValidator::has_array_access($attributes) === false || InputValidator::is_iterable($attributes) === false) {
			throw InvalidArgument::create(3, '$attributes', 'array|ArrayAccess&Traversable', gettype($attributes));
		}

		if (is_array($flags) === false) {
			throw InvalidArgument::create(4, '$flags', 'array', gettype($flags));
		}

		if ($reference_time !== null && is_int($reference_time) === false) {
			throw InvalidArgument::create(5, '$reference_time', 'integer|null', gettype($reference_time));
		}

		$this->name       = $name;
		$this->value      = $value;
		$this->attributes = $attributes;
		$default_flags    = [
			'creation'    => time(),
			'last-access' => time(),
			'persistent'  => false,
			'host-only'   => true,
		];
		$this->flags      = array_merge($default_flags, $flags);

		$this->reference_time = time();
		if ($reference_time !== null) {
			$this->reference_time = $reference_time;
		}

		$this->normalize();
	}

	/**
	 * Get the cookie value
	 *
	 * Attributes and other data can be accessed via methods.
	 */
	public function __toString() {
		return $this->value;
	}

	/**
	 * Check if a cookie is expired.
	 *
	 * Checks the age against $this->reference_time to determine if the cookie
	 * is expired.
	 *
	 * @return boolean True if expired, false if time is valid.
	 */
	public function is_expired() {
		// RFC6265, s. 4.1.2.2:
		// If a cookie has both the Max-Age and the Expires attribute, the Max-
		// Age attribute has precedence and controls the expiration date of the
		// cookie.
		if (isset($this->attributes['max-age'])) {
			$max_age = $this->attributes['max-age'];
			return $max_age < $this->reference_time;
		}

		if (isset($this->attributes['expires'])) {
			$expires = $this->attributes['expires'];
			return $expires < $this->reference_time;
		}

		return false;
	}

	/**
	 * Check if a cookie is valid for a given URI
	 *
	 * @param \WpOrg\Requests\Iri $uri URI to check
	 * @return boolean Whether the cookie is valid for the given URI
	 */
	public function uri_matches(Iri $uri) {
		if (!$this->domain_matches($uri->host)) {
			return false;
		}

		if (!$this->path_matches($uri->path)) {
			return false;
		}

		return empty($this->attributes['secure']) || $uri->scheme === 'https';
	}

	/**
	 * Check if a cookie is valid for a given domain
	 *
	 * @param string $domain Domain to check
	 * @return boolean Whether the cookie is valid for the given domain
	 */
	public function domain_matches($domain) {
		if (is_string($domain) === false) {
			return false;
		}

		if (!isset($this->attributes['domain'])) {
			// Cookies created manually; cookies created by Requests will set
			// the domain to the requested domain
			return true;
		}

		$cookie_domain = $this->attributes['domain'];
		if ($cookie_domain === $domain) {
			// The cookie domain and the passed domain are identical.
			return true;
		}

		// If the cookie is marked as host-only and we don't have an exact
		// match, reject the cookie
		if ($this->flags['host-only'] === true) {
			return false;
		}

		if (strlen($domain) <= strlen($cookie_domain)) {
			// For obvious reasons, the cookie domain cannot be a suffix if the passed domain
			// is shorter than the cookie domain
			return false;
		}

		if (substr($domain, -1 * strlen($cookie_domain)) !== $cookie_domain) {
			// The cookie domain should be a suffix of the passed domain.
			return false;
		}

		$prefix = substr($domain, 0, strlen($domain) - strlen($cookie_domain));
		if (substr($prefix, -1) !== '.') {
			// The last character of the passed domain that is not included in the
			// domain string should be a %x2E (".") character.
			return false;
		}

		// The passed domain should be a host name (i.e., not an IP address).
		return !preg_match('#^(.+\.)\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$#', $domain);
	}

	/**
	 * Check if a cookie is valid for a given path
	 *
	 * From the path-match check in RFC 6265 section 5.1.4
	 *
	 * @param string $request_path Path to check
	 * @return boolean Whether the cookie is valid for the given path
	 */
	public function path_matches($request_path) {
		if (empty($request_path)) {
			// Normalize empty path to root
			$request_path = '/';
		}

		if (!isset($this->attributes['path'])) {
			// Cookies created manually; cookies created by Requests will set
			// the path to the requested path
			return true;
		}

		if (is_scalar($request_path) === false) {
			return false;
		}

		$cookie_path = $this->attributes['path'];

		if ($cookie_path === $request_path) {
			// The cookie-path and the request-path are identical.
			return true;
		}

		if (strlen($request_path) > strlen($cookie_path) && substr($request_path, 0, strlen($cookie_path)) === $cookie_path) {
			if (substr($cookie_path, -1) === '/') {
				// The cookie-path is a prefix of the request-path, and the last
				// character of the cookie-path is %x2F ("/").
				return true;
			}

			if (substr($request_path, strlen($cookie_path), 1) === '/') {
				// The cookie-path is a prefix of the request-path, and the
				// first character of the request-path that is not included in
				// the cookie-path is a %x2F ("/") character.
				return true;
			}
		}

		return false;
	}

	/**
	 * Normalize cookie and attributes
	 *
	 * @return boolean Whether the cookie was successfully normalized
	 */
	public function normalize() {
		foreach ($this->attributes as $key => $value) {
			$orig_value = $value;

			if (is_string($key)) {
				$value = $this->normalize_attribute($key, $value);
			}

			if ($value === null) {
				unset($this->attributes[$key]);
				continue;
			}

			if ($value !== $orig_value) {
				$this->attributes[$key] = $value;
			}
		}

		return true;
	}

	/**
	 * Parse an individual cookie attribute
	 *
	 * Handles parsing individual attributes from the cookie values.
	 *
	 * @param string $name Attribute name
	 * @param string|int|bool $value Attribute value (string/integer value, or true if empty/flag)
	 * @return mixed Value if available, or null if the attribute value is invalid (and should be skipped)
	 */
	protected function normalize_attribute($name, $value) {
		switch (strtolower($name)) {
			case 'expires':
				// Expiration parsing, as per RFC 6265 section 5.2.1
				if (is_int($value)) {
					return $value;
				}

				$expiry_time = strtotime($value);
				if ($expiry_time === false) {
					return null;
				}

				return $expiry_time;

			case 'max-age':
				// Expiration parsing, as per RFC 6265 section 5.2.2
				if (is_int($value)) {
					return $value;
				}

				// Check that we have a valid age
				if (!preg_match('/^-?\d+$/', $value)) {
					return null;
				}

				$delta_seconds = (int) $value;
				if ($delta_seconds <= 0) {
					$expiry_time = 0;
				} else {
					$expiry_time = $this->reference_time + $delta_seconds;
				}

				return $expiry_time;

			case 'domain':
				// Domains are not required as per RFC 6265 section 5.2.3
				if (empty($value)) {
					return null;
				}

				// Domain normalization, as per RFC 6265 section 5.2.3
				if ($value[0] === '.') {
					$value = substr($value, 1);
				}

				return $value;

			default:
				return $value;
		}
	}

	/**
	 * Format a cookie for a Cookie header
	 *
	 * This is used when sending cookies to a server.
	 *
	 * @return string Cookie formatted for Cookie header
	 */
	public function format_for_header() {
		return sprintf('%s=%s', $this->name, $this->value);
	}

	/**
	 * Format a cookie for a Set-Cookie header
	 *
	 * This is used when sending cookies to clients. This isn't really
	 * applicable to client-side usage, but might be handy for debugging.
	 *
	 * @return string Cookie formatted for Set-Cookie header
	 */
	public function format_for_set_cookie() {
		$header_value = $this->format_for_header();
		if (!empty($this->attributes)) {
			$parts = [];
			foreach ($this->attributes as $key => $value) {
				// Ignore non-associative attributes
				if (is_numeric($key)) {
					$parts[] = $value;
				} else {
					$parts[] = sprintf('%s=%s', $key, $value);
				}
			}

			$header_value .= '; ' . implode('; ', $parts);
		}

		return $header_value;
	}

	/**
	 * Parse a cookie string into a cookie object
	 *
	 * Based on Mozilla's parsing code in Firefox and related projects, which
	 * is an intentional deviation from RFC 2109 and RFC 2616. RFC 6265
	 * specifies some of this handling, but not in a thorough manner.
	 *
	 * @param string $cookie_header Cookie header value (from a Set-Cookie header)
	 * @param string $name
	 * @param int|null $reference_time
	 * @return \WpOrg\Requests\Cookie Parsed cookie object
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cookie_header argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $name argument is not a string.
	 */
	public static function parse($cookie_header, $name = '', $reference_time = null) {
		if (is_string($cookie_header) === false) {
			throw InvalidArgument::create(1, '$cookie_header', 'string', gettype($cookie_header));
		}

		if (is_string($name) === false) {
			throw InvalidArgument::create(2, '$name', 'string', gettype($name));
		}

		$parts   = explode(';', $cookie_header);
		$kvparts = array_shift($parts);

		if (!empty($name)) {
			$value = $cookie_header;
		} elseif (strpos($kvparts, '=') === false) {
			// Some sites might only have a value without the equals separator.
			// Deviate from RFC 6265 and pretend it was actually a blank name
			// (`=foo`)
			//
			// https://bugzilla.mozilla.org/show_bug.cgi?id=169091
			$name  = '';
			$value = $kvparts;
		} else {
			list($name, $value) = explode('=', $kvparts, 2);
		}

		$name  = trim($name);
		$value = trim($value);

		// Attribute keys are handled case-insensitively
		$attributes = new CaseInsensitiveDictionary();

		if (!empty($parts)) {
			foreach ($parts as $part) {
				if (strpos($part, '=') === false) {
					$part_key   = $part;
					$part_value = true;
				} else {
					list($part_key, $part_value) = explode('=', $part, 2);
					$part_value                  = trim($part_value);
				}

				$part_key              = trim($part_key);
				$attributes[$part_key] = $part_value;
			}
		}

		return new static($name, $value, $attributes, [], $reference_time);
	}

	/**
	 * Parse all Set-Cookie headers from request headers
	 *
	 * @param \WpOrg\Requests\Response\Headers $headers Headers to parse from
	 * @param \WpOrg\Requests\Iri|null $origin URI for comparing cookie origins
	 * @param int|null $time Reference time for expiration calculation
	 * @return array
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $origin argument is not null or an instance of the Iri class.
	 */
	public static function parse_from_headers(Headers $headers, $origin = null, $time = null) {
		$cookie_headers = $headers->getValues('Set-Cookie');
		if (empty($cookie_headers)) {
			return [];
		}

		if ($origin !== null && !($origin instanceof Iri)) {
			throw InvalidArgument::create(2, '$origin', Iri::class . ' or null', gettype($origin));
		}

		$cookies = [];
		foreach ($cookie_headers as $header) {
			$parsed = self::parse($header, '', $time);

			// Default domain/path attributes
			if (empty($parsed->attributes['domain']) && !empty($origin)) {
				$parsed->attributes['domain'] = $origin->host;
				$parsed->flags['host-only']   = true;
			} else {
				$parsed->flags['host-only'] = false;
			}

			$path_is_valid = (!empty($parsed->attributes['path']) && $parsed->attributes['path'][0] === '/');
			if (!$path_is_valid && !empty($origin)) {
				$path = $origin->path;

				// Default path normalization as per RFC 6265 section 5.1.4
				if (substr($path, 0, 1) !== '/') {
					// If the uri-path is empty or if the first character of
					// the uri-path is not a %x2F ("/") character, output
					// %x2F ("/") and skip the remaining steps.
					$path = '/';
				} elseif (substr_count($path, '/') === 1) {
					// If the uri-path contains no more than one %x2F ("/")
					// character, output %x2F ("/") and skip the remaining
					// step.
					$path = '/';
				} else {
					// Output the characters of the uri-path from the first
					// character up to, but not including, the right-most
					// %x2F ("/").
					$path = substr($path, 0, strrpos($path, '/'));
				}

				$parsed->attributes['path'] = $path;
			}

			// Reject invalid cookie domains
			if (!empty($origin) && !$parsed->domain_matches($origin->host)) {
				continue;
			}

			$cookies[$parsed->name] = $parsed;
		}

		return $cookies;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   Response/Headers.php                                                                                0000644                 00000006046 15227642210 0010435 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Case-insensitive dictionary, suitable for HTTP headers
 *
 * @package Requests
 */

namespace WpOrg\Requests\Response;

use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
use WpOrg\Requests\Utility\FilteredIterator;

/**
 * Case-insensitive dictionary, suitable for HTTP headers
 *
 * @package Requests
 */
class Headers extends CaseInsensitiveDictionary {
	/**
	 * Get the given header
	 *
	 * Unlike {@see \WpOrg\Requests\Response\Headers::getValues()}, this returns a string. If there are
	 * multiple values, it concatenates them with a comma as per RFC2616.
	 *
	 * Avoid using this where commas may be used unquoted in values, such as
	 * Set-Cookie headers.
	 *
	 * @param string $offset Name of the header to retrieve.
	 * @return string|null Header value
	 */
	public function offsetGet($offset) {
		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if (!isset($offset, $this->data[$offset])) {
			return null;
		}

		return $this->flatten($this->data[$offset]);
	}

	/**
	 * Set the given item
	 *
	 * @param string $offset Item name
	 * @param string $value Item value
	 *
	 * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`)
	 */
	public function offsetSet($offset, $value) {
		if ($offset === null) {
			throw new Exception('Object is a dictionary, not a list', 'invalidset');
		}

		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if (!isset($this->data[$offset])) {
			$this->data[$offset] = [];
		}

		$this->data[$offset][] = $value;
	}

	/**
	 * Get all values for a given header
	 *
	 * @param string $offset Name of the header to retrieve.
	 * @return array|null Header values
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not valid as an array key.
	 */
	public function getValues($offset) {
		if (!is_string($offset) && !is_int($offset)) {
			throw InvalidArgument::create(1, '$offset', 'string|int', gettype($offset));
		}

		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if (!isset($this->data[$offset])) {
			return null;
		}

		return $this->data[$offset];
	}

	/**
	 * Flattens a value into a string
	 *
	 * Converts an array into a string by imploding values with a comma, as per
	 * RFC2616's rules for folding headers.
	 *
	 * @param string|array $value Value to flatten
	 * @return string Flattened value
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or an array.
	 */
	public function flatten($value) {
		if (is_string($value)) {
			return $value;
		}

		if (is_array($value)) {
			return implode(',', $value);
		}

		throw InvalidArgument::create(1, '$value', 'string|array', gettype($value));
	}

	/**
	 * Get an iterator for the data
	 *
	 * Converts the internally stored values to a comma-separated string if there is more
	 * than one value for a key.
	 *
	 * @return \ArrayIterator
	 */
	public function getIterator() {
		return new FilteredIterator($this->data, [$this, 'flatten']);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          Response/error_log                                                                                  0000644                 00000001230 15227642210 0010254 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 16:15:47 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Response/Headers.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Response/Headers.php on line 20
[19-Jul-2026 09:18:48 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Utility\CaseInsensitiveDictionary" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Response/Headers.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Response/Headers.php on line 20
                                                                                                                                                                                                                                                                                                                                                                        IdnaEncoder.php                                                                                     0000644                 00000030237 15227642210 0007436 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace WpOrg\Requests;

use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\InputValidator;

/**
 * IDNA URL encoder
 *
 * Note: Not fully compliant, as nameprep does nothing yet.
 *
 * @package Requests\Utilities
 *
 * @link https://tools.ietf.org/html/rfc3490 IDNA specification
 * @link https://tools.ietf.org/html/rfc3492 Punycode/Bootstrap specification
 */
class IdnaEncoder {
	/**
	 * ACE prefix used for IDNA
	 *
	 * @link https://tools.ietf.org/html/rfc3490#section-5
	 * @var string
	 */
	const ACE_PREFIX = 'xn--';

	/**
	 * Maximum length of a IDNA URL in ASCII.
	 *
	 * @see \WpOrg\Requests\IdnaEncoder::to_ascii()
	 *
	 * @since 2.0.0
	 *
	 * @var int
	 */
	const MAX_LENGTH = 64;

	/**#@+
	 * Bootstrap constant for Punycode
	 *
	 * @link https://tools.ietf.org/html/rfc3492#section-5
	 * @var int
	 */
	const BOOTSTRAP_BASE         = 36;
	const BOOTSTRAP_TMIN         = 1;
	const BOOTSTRAP_TMAX         = 26;
	const BOOTSTRAP_SKEW         = 38;
	const BOOTSTRAP_DAMP         = 700;
	const BOOTSTRAP_INITIAL_BIAS = 72;
	const BOOTSTRAP_INITIAL_N    = 128;
	/**#@-*/

	/**
	 * Encode a hostname using Punycode
	 *
	 * @param string|Stringable $hostname Hostname
	 * @return string Punycode-encoded hostname
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object.
	 */
	public static function encode($hostname) {
		if (InputValidator::is_string_or_stringable($hostname) === false) {
			throw InvalidArgument::create(1, '$hostname', 'string|Stringable', gettype($hostname));
		}

		$parts = explode('.', $hostname);
		foreach ($parts as &$part) {
			$part = self::to_ascii($part);
		}

		return implode('.', $parts);
	}

	/**
	 * Convert a UTF-8 text string to an ASCII string using Punycode
	 *
	 * @param string $text ASCII or UTF-8 string (max length 64 characters)
	 * @return string ASCII string
	 *
	 * @throws \WpOrg\Requests\Exception Provided string longer than 64 ASCII characters (`idna.provided_too_long`)
	 * @throws \WpOrg\Requests\Exception Prepared string longer than 64 ASCII characters (`idna.prepared_too_long`)
	 * @throws \WpOrg\Requests\Exception Provided string already begins with xn-- (`idna.provided_is_prefixed`)
	 * @throws \WpOrg\Requests\Exception Encoded string longer than 64 ASCII characters (`idna.encoded_too_long`)
	 */
	public static function to_ascii($text) {
		// Step 1: Check if the text is already ASCII
		if (self::is_ascii($text)) {
			// Skip to step 7
			if (strlen($text) < self::MAX_LENGTH) {
				return $text;
			}

			throw new Exception('Provided string is too long', 'idna.provided_too_long', $text);
		}

		// Step 2: nameprep
		$text = self::nameprep($text);

		// Step 3: UseSTD3ASCIIRules is false, continue
		// Step 4: Check if it's ASCII now
		if (self::is_ascii($text)) {
			// Skip to step 7
			/*
			 * As the `nameprep()` method returns the original string, this code will never be reached until
			 * that method is properly implemented.
			 */
			// @codeCoverageIgnoreStart
			if (strlen($text) < self::MAX_LENGTH) {
				return $text;
			}

			throw new Exception('Prepared string is too long', 'idna.prepared_too_long', $text);
			// @codeCoverageIgnoreEnd
		}

		// Step 5: Check ACE prefix
		if (strpos($text, self::ACE_PREFIX) === 0) {
			throw new Exception('Provided string begins with ACE prefix', 'idna.provided_is_prefixed', $text);
		}

		// Step 6: Encode with Punycode
		$text = self::punycode_encode($text);

		// Step 7: Prepend ACE prefix
		$text = self::ACE_PREFIX . $text;

		// Step 8: Check size
		if (strlen($text) < self::MAX_LENGTH) {
			return $text;
		}

		throw new Exception('Encoded string is too long', 'idna.encoded_too_long', $text);
	}

	/**
	 * Check whether a given text string contains only ASCII characters
	 *
	 * @internal (Testing found regex was the fastest implementation)
	 *
	 * @param string $text Text to examine.
	 * @return bool Is the text string ASCII-only?
	 */
	protected static function is_ascii($text) {
		return (preg_match('/(?:[^\x00-\x7F])/', $text) !== 1);
	}

	/**
	 * Prepare a text string for use as an IDNA name
	 *
	 * @todo Implement this based on RFC 3491 and the newer 5891
	 * @param string $text Text to prepare.
	 * @return string Prepared string
	 */
	protected static function nameprep($text) {
		return $text;
	}

	/**
	 * Convert a UTF-8 string to a UCS-4 codepoint array
	 *
	 * Based on \WpOrg\Requests\Iri::replace_invalid_with_pct_encoding()
	 *
	 * @param string $input Text to convert.
	 * @return array Unicode code points
	 *
	 * @throws \WpOrg\Requests\Exception Invalid UTF-8 codepoint (`idna.invalidcodepoint`)
	 */
	protected static function utf8_to_codepoints($input) {
		$codepoints = [];

		// Get number of bytes
		$strlen = strlen($input);

		// phpcs:ignore Generic.CodeAnalysis.JumbledIncrementer -- This is a deliberate choice.
		for ($position = 0; $position < $strlen; $position++) {
			$value = ord($input[$position]);

			if ((~$value & 0x80) === 0x80) {            // One byte sequence:
				$character = $value;
				$length    = 1;
				$remaining = 0;
			} elseif (($value & 0xE0) === 0xC0) {       // Two byte sequence:
				$character = ($value & 0x1F) << 6;
				$length    = 2;
				$remaining = 1;
			} elseif (($value & 0xF0) === 0xE0) {       // Three byte sequence:
				$character = ($value & 0x0F) << 12;
				$length    = 3;
				$remaining = 2;
			} elseif (($value & 0xF8) === 0xF0) {       // Four byte sequence:
				$character = ($value & 0x07) << 18;
				$length    = 4;
				$remaining = 3;
			} else {                                    // Invalid byte:
				throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $value);
			}

			if ($remaining > 0) {
				if ($position + $length > $strlen) {
					throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
				}

				for ($position++; $remaining > 0; $position++) {
					$value = ord($input[$position]);

					// If it is invalid, count the sequence as invalid and reprocess the current byte:
					if (($value & 0xC0) !== 0x80) {
						throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
					}

					--$remaining;
					$character |= ($value & 0x3F) << ($remaining * 6);
				}

				$position--;
			}

			if (// Non-shortest form sequences are invalid
				($length > 1 && $character <= 0x7F)
				|| ($length > 2 && $character <= 0x7FF)
				|| ($length > 3 && $character <= 0xFFFF)
				// Outside of range of ucschar codepoints
				// Noncharacters
				|| ($character & 0xFFFE) === 0xFFFE
				|| ($character >= 0xFDD0 && $character <= 0xFDEF)
				|| (
					// Everything else not in ucschar
					($character > 0xD7FF && $character < 0xF900)
					|| $character < 0x20
					|| ($character > 0x7E && $character < 0xA0)
					|| $character > 0xEFFFD
				)
			) {
				throw new Exception('Invalid Unicode codepoint', 'idna.invalidcodepoint', $character);
			}

			$codepoints[] = $character;
		}

		return $codepoints;
	}

	/**
	 * RFC3492-compliant encoder
	 *
	 * @internal Pseudo-code from Section 6.3 is commented with "#" next to relevant code
	 *
	 * @param string $input UTF-8 encoded string to encode
	 * @return string Punycode-encoded string
	 *
	 * @throws \WpOrg\Requests\Exception On character outside of the domain (never happens with Punycode) (`idna.character_outside_domain`)
	 */
	public static function punycode_encode($input) {
		$output = '';
		// let n = initial_n
		$n = self::BOOTSTRAP_INITIAL_N;
		// let delta = 0
		$delta = 0;
		// let bias = initial_bias
		$bias = self::BOOTSTRAP_INITIAL_BIAS;
		// let h = b = the number of basic code points in the input
		$h = 0;
		$b = 0; // see loop
		// copy them to the output in order
		$codepoints = self::utf8_to_codepoints($input);
		$extended   = [];

		foreach ($codepoints as $char) {
			if ($char < 128) {
				// Character is valid ASCII
				// TODO: this should also check if it's valid for a URL
				$output .= chr($char);
				$h++;

				// Check if the character is non-ASCII, but below initial n
				// This never occurs for Punycode, so ignore in coverage
				// @codeCoverageIgnoreStart
			} elseif ($char < $n) {
				throw new Exception('Invalid character', 'idna.character_outside_domain', $char);
				// @codeCoverageIgnoreEnd
			} else {
				$extended[$char] = true;
			}
		}

		$extended = array_keys($extended);
		sort($extended);
		$b = $h;
		// [copy them] followed by a delimiter if b > 0
		if (strlen($output) > 0) {
			$output .= '-';
		}

		// {if the input contains a non-basic code point < n then fail}
		// while h < length(input) do begin
		$codepointcount = count($codepoints);
		while ($h < $codepointcount) {
			// let m = the minimum code point >= n in the input
			$m = array_shift($extended);
			//printf('next code point to insert is %s' . PHP_EOL, dechex($m));
			// let delta = delta + (m - n) * (h + 1), fail on overflow
			$delta += ($m - $n) * ($h + 1);
			// let n = m
			$n = $m;
			// for each code point c in the input (in order) do begin
			for ($num = 0; $num < $codepointcount; $num++) {
				$c = $codepoints[$num];
				// if c < n then increment delta, fail on overflow
				if ($c < $n) {
					$delta++;
				} elseif ($c === $n) { // if c == n then begin
					// let q = delta
					$q = $delta;
					// for k = base to infinity in steps of base do begin
					for ($k = self::BOOTSTRAP_BASE; ; $k += self::BOOTSTRAP_BASE) {
						// let t = tmin if k <= bias {+ tmin}, or
						//     tmax if k >= bias + tmax, or k - bias otherwise
						if ($k <= ($bias + self::BOOTSTRAP_TMIN)) {
							$t = self::BOOTSTRAP_TMIN;
						} elseif ($k >= ($bias + self::BOOTSTRAP_TMAX)) {
							$t = self::BOOTSTRAP_TMAX;
						} else {
							$t = $k - $bias;
						}

						// if q < t then break
						if ($q < $t) {
							break;
						}

						// output the code point for digit t + ((q - t) mod (base - t))
						$digit   = (int) ($t + (($q - $t) % (self::BOOTSTRAP_BASE - $t)));
						$output .= self::digit_to_char($digit);
						// let q = (q - t) div (base - t)
						$q = (int) floor(($q - $t) / (self::BOOTSTRAP_BASE - $t));
					} // end
					// output the code point for digit q
					$output .= self::digit_to_char($q);
					// let bias = adapt(delta, h + 1, test h equals b?)
					$bias = self::adapt($delta, $h + 1, $h === $b);
					// let delta = 0
					$delta = 0;
					// increment h
					$h++;
				} // end
			} // end
			// increment delta and n
			$delta++;
			$n++;
		} // end

		return $output;
	}

	/**
	 * Convert a digit to its respective character
	 *
	 * @link https://tools.ietf.org/html/rfc3492#section-5
	 *
	 * @param int $digit Digit in the range 0-35
	 * @return string Single character corresponding to digit
	 *
	 * @throws \WpOrg\Requests\Exception On invalid digit (`idna.invalid_digit`)
	 */
	protected static function digit_to_char($digit) {
		// @codeCoverageIgnoreStart
		// As far as I know, this never happens, but still good to be sure.
		if ($digit < 0 || $digit > 35) {
			throw new Exception(sprintf('Invalid digit %d', $digit), 'idna.invalid_digit', $digit);
		}

		// @codeCoverageIgnoreEnd
		$digits = 'abcdefghijklmnopqrstuvwxyz0123456789';
		return substr($digits, $digit, 1);
	}

	/**
	 * Adapt the bias
	 *
	 * @link https://tools.ietf.org/html/rfc3492#section-6.1
	 * @param int $delta
	 * @param int $numpoints
	 * @param bool $firsttime
	 * @return int|float New bias
	 *
	 * function adapt(delta,numpoints,firsttime):
	 */
	protected static function adapt($delta, $numpoints, $firsttime) {
		// if firsttime then let delta = delta div damp
		if ($firsttime) {
			$delta = floor($delta / self::BOOTSTRAP_DAMP);
		} else {
			// else let delta = delta div 2
			$delta = floor($delta / 2);
		}

		// let delta = delta + (delta div numpoints)
		$delta += floor($delta / $numpoints);
		// let k = 0
		$k = 0;
		// while delta > ((base - tmin) * tmax) div 2 do begin
		$max = floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN) * self::BOOTSTRAP_TMAX) / 2);
		while ($delta > $max) {
			// let delta = delta div (base - tmin)
			$delta = floor($delta / (self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN));
			// let k = k + base
			$k += self::BOOTSTRAP_BASE;
		} // end
		// return k + (((base - tmin + 1) * delta) div (delta + skew))
		return $k + floor(((self::BOOTSTRAP_BASE - self::BOOTSTRAP_TMIN + 1) * $delta) / ($delta + self::BOOTSTRAP_SKEW));
	}
}
                                                                                                                                                                                                                                                                                                                                                                 Transport/Curl.php                                                                                  0000644                 00000046646 15227642210 0010177 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * cURL HTTP transport
 *
 * @package Requests\Transport
 */

namespace WpOrg\Requests\Transport;

use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use WpOrg\Requests\Capability;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Exception\Transport\Curl as CurlException;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Transport;
use WpOrg\Requests\Utility\InputValidator;

/**
 * cURL HTTP transport
 *
 * @package Requests\Transport
 */
final class Curl implements Transport {
	const CURL_7_10_5 = 0x070A05;
	const CURL_7_16_2 = 0x071002;

	/**
	 * Raw HTTP data
	 *
	 * @var string
	 */
	public $headers = '';

	/**
	 * Raw body data
	 *
	 * @var string
	 */
	public $response_data = '';

	/**
	 * Information on the current request
	 *
	 * @var array cURL information array, see {@link https://www.php.net/curl_getinfo}
	 */
	public $info;

	/**
	 * cURL version number
	 *
	 * @var int
	 */
	public $version;

	/**
	 * cURL handle
	 *
	 * @var resource|\CurlHandle Resource in PHP < 8.0, Instance of CurlHandle in PHP >= 8.0.
	 */
	private $handle;

	/**
	 * Hook dispatcher instance
	 *
	 * @var \WpOrg\Requests\Hooks
	 */
	private $hooks;

	/**
	 * Have we finished the headers yet?
	 *
	 * @var boolean
	 */
	private $done_headers = false;

	/**
	 * If streaming to a file, keep the file pointer
	 *
	 * @var resource
	 */
	private $stream_handle;

	/**
	 * How many bytes are in the response body?
	 *
	 * @var int
	 */
	private $response_bytes;

	/**
	 * What's the maximum number of bytes we should keep?
	 *
	 * @var int|bool Byte count, or false if no limit.
	 */
	private $response_byte_limit;

	/**
	 * Constructor
	 */
	public function __construct() {
		$curl          = curl_version();
		$this->version = $curl['version_number'];
		$this->handle  = curl_init();

		curl_setopt($this->handle, CURLOPT_HEADER, false);
		curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, 1);
		if ($this->version >= self::CURL_7_10_5) {
			curl_setopt($this->handle, CURLOPT_ENCODING, '');
		}

		if (defined('CURLOPT_PROTOCOLS')) {
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_protocolsFound
			curl_setopt($this->handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
		}

		if (defined('CURLOPT_REDIR_PROTOCOLS')) {
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_redir_protocolsFound
			curl_setopt($this->handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
		}
	}

	/**
	 * Destructor
	 */
	public function __destruct() {
		if (is_resource($this->handle)) {
			// phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.curl_closeDeprecated,Generic.PHP.DeprecatedFunctions.Deprecated
			curl_close($this->handle);
		}
	}

	/**
	 * Perform a request
	 *
	 * @param string|Stringable $url URL to request
	 * @param array $headers Associative array of request headers
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
	 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return string Raw HTTP result
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 * @throws \WpOrg\Requests\Exception       On a cURL error (`curlerror`)
	 */
	public function request($url, $headers = [], $data = [], $options = []) {
		if (InputValidator::is_string_or_stringable($url) === false) {
			throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url));
		}

		if (is_array($headers) === false) {
			throw InvalidArgument::create(2, '$headers', 'array', gettype($headers));
		}

		if (!is_array($data) && !is_string($data)) {
			if ($data === null) {
				$data = '';
			} else {
				throw InvalidArgument::create(3, '$data', 'array|string', gettype($data));
			}
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(4, '$options', 'array', gettype($options));
		}

		$this->hooks = $options['hooks'];

		$this->setup_handle($url, $headers, $data, $options);

		$options['hooks']->dispatch('curl.before_send', [&$this->handle]);

		if ($options['filename'] !== false) {
			// phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception.
			$this->stream_handle = @fopen($options['filename'], 'wb');
			if ($this->stream_handle === false) {
				$error = error_get_last();
				throw new Exception($error['message'], 'fopen');
			}
		}

		$this->response_data       = '';
		$this->response_bytes      = 0;
		$this->response_byte_limit = false;
		if ($options['max_bytes'] !== false) {
			$this->response_byte_limit = $options['max_bytes'];
		}

		if (isset($options['verify'])) {
			if ($options['verify'] === false) {
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
				curl_setopt($this->handle, CURLOPT_SSL_VERIFYPEER, 0);
			} elseif (is_string($options['verify'])) {
				curl_setopt($this->handle, CURLOPT_CAINFO, $options['verify']);
			}
		}

		if (isset($options['verifyname']) && $options['verifyname'] === false) {
			curl_setopt($this->handle, CURLOPT_SSL_VERIFYHOST, 0);
		}

		curl_exec($this->handle);
		$response = $this->response_data;

		$options['hooks']->dispatch('curl.after_send', []);

		if (curl_errno($this->handle) === CURLE_WRITE_ERROR || curl_errno($this->handle) === CURLE_BAD_CONTENT_ENCODING) {
			// Reset encoding and try again
			curl_setopt($this->handle, CURLOPT_ENCODING, 'none');

			$this->response_data  = '';
			$this->response_bytes = 0;
			curl_exec($this->handle);
			$response = $this->response_data;
		}

		$this->process_response($response, $options);

		// Need to remove the $this reference from the curl handle.
		// Otherwise \WpOrg\Requests\Transport\Curl won't be garbage collected and the curl_close() will never be called.
		curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, null);
		curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, null);

		return $this->headers;
	}

	/**
	 * Send multiple requests simultaneously
	 *
	 * @param array $requests Request data
	 * @param array $options Global options
	 * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well)
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 */
	public function request_multiple($requests, $options) {
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
		if (empty($requests)) {
			return [];
		}

		if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
			throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(2, '$options', 'array', gettype($options));
		}

		$multihandle = curl_multi_init();
		$subrequests = [];
		$subhandles  = [];

		$class = get_class($this);
		foreach ($requests as $id => $request) {
			$subrequests[$id] = new $class();
			$subhandles[$id]  = $subrequests[$id]->get_subrequest_handle($request['url'], $request['headers'], $request['data'], $request['options']);
			$request['options']['hooks']->dispatch('curl.before_multi_add', [&$subhandles[$id]]);
			curl_multi_add_handle($multihandle, $subhandles[$id]);
		}

		$completed       = 0;
		$responses       = [];
		$subrequestcount = count($subrequests);

		$request['options']['hooks']->dispatch('curl.before_multi_exec', [&$multihandle]);

		do {
			$active = 0;

			do {
				$status = curl_multi_exec($multihandle, $active);
			} while ($status === CURLM_CALL_MULTI_PERFORM);

			$to_process = [];

			// Read the information as needed
			while ($done = curl_multi_info_read($multihandle)) {
				$key = array_search($done['handle'], $subhandles, true);
				if (!isset($to_process[$key])) {
					$to_process[$key] = $done;
				}
			}

			// Parse the finished requests before we start getting the new ones
			foreach ($to_process as $key => $done) {
				$options = $requests[$key]['options'];
				if ($done['result'] !== CURLE_OK) {
					//get error string for handle.
					$reason          = curl_error($done['handle']);
					$exception       = new CurlException(
						$reason,
						CurlException::EASY,
						$done['handle'],
						$done['result']
					);
					$responses[$key] = $exception;
					$options['hooks']->dispatch('transport.internal.parse_error', [&$responses[$key], $requests[$key]]);
				} else {
					$responses[$key] = $subrequests[$key]->process_response($subrequests[$key]->response_data, $options);

					$options['hooks']->dispatch('transport.internal.parse_response', [&$responses[$key], $requests[$key]]);
				}

				curl_multi_remove_handle($multihandle, $done['handle']);
				if (is_resource($done['handle'])) {
					// phpcs:ignore PHPCompatibility.FunctionUse.RemovedFunctions.curl_closeDeprecated,Generic.PHP.DeprecatedFunctions.Deprecated
					curl_close($done['handle']);
				}

				if (!is_string($responses[$key])) {
					$options['hooks']->dispatch('multiple.request.complete', [&$responses[$key], $key]);
				}

				$completed++;
			}
		} while ($active || $completed < $subrequestcount);

		$request['options']['hooks']->dispatch('curl.after_multi_exec', [&$multihandle]);

		curl_multi_close($multihandle);

		return $responses;
	}

	/**
	 * Get the cURL handle for use in a multi-request
	 *
	 * @param string $url URL to request
	 * @param array $headers Associative array of request headers
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
	 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return resource|\CurlHandle Subrequest's cURL handle
	 */
	public function &get_subrequest_handle($url, $headers, $data, $options) {
		$this->setup_handle($url, $headers, $data, $options);

		if ($options['filename'] !== false) {
			$this->stream_handle = fopen($options['filename'], 'wb');
		}

		$this->response_data       = '';
		$this->response_bytes      = 0;
		$this->response_byte_limit = false;
		if ($options['max_bytes'] !== false) {
			$this->response_byte_limit = $options['max_bytes'];
		}

		$this->hooks = $options['hooks'];

		return $this->handle;
	}

	/**
	 * Setup the cURL handle for the given data
	 *
	 * @param string $url URL to request
	 * @param array $headers Associative array of request headers
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
	 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 */
	private function setup_handle($url, $headers, $data, $options) {
		$options['hooks']->dispatch('curl.before_request', [&$this->handle]);

		// Force closing the connection for old versions of cURL (<7.22).
		if (!isset($headers['Connection'])) {
			$headers['Connection'] = 'close';
		}

		/**
		 * Add "Expect" header.
		 *
		 * By default, cURL adds a "Expect: 100-Continue" to most requests. This header can
		 * add as much as a second to the time it takes for cURL to perform a request. To
		 * prevent this, we need to set an empty "Expect" header. To match the behaviour of
		 * Guzzle, we'll add the empty header to requests that are smaller than 1 MB and use
		 * HTTP/1.1.
		 *
		 * https://curl.se/mail/lib-2017-07/0013.html
		 */
		if (!isset($headers['Expect']) && $options['protocol_version'] === 1.1) {
			$headers['Expect'] = $this->get_expect_header($data);
		}

		$headers = Requests::flatten($headers);

		if (!empty($data)) {
			$data_format = $options['data_format'];

			if ($data_format === 'query') {
				$url  = self::format_get($url, $data);
				$data = '';
			} elseif (!is_string($data)) {
				$data = http_build_query($data, '', '&');
			}
		}

		switch ($options['type']) {
			case Requests::POST:
				curl_setopt($this->handle, CURLOPT_POST, true);
				curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
				break;
			case Requests::HEAD:
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
				curl_setopt($this->handle, CURLOPT_NOBODY, true);
				break;
			case Requests::TRACE:
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
				break;
			case Requests::PATCH:
			case Requests::PUT:
			case Requests::DELETE:
			case Requests::OPTIONS:
			default:
				curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, $options['type']);
				if (!empty($data)) {
					curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
				}
		}

		// cURL requires a minimum timeout of 1 second when using the system
		// DNS resolver, as it uses `alarm()`, which is second resolution only.
		// There's no way to detect which DNS resolver is being used from our
		// end, so we need to round up regardless of the supplied timeout.
		//
		// https://github.com/curl/curl/blob/4f45240bc84a9aa648c8f7243be7b79e9f9323a5/lib/hostip.c#L606-L609
		$timeout = max($options['timeout'], 1);

		if (is_int($timeout) || $this->version < self::CURL_7_16_2) {
			curl_setopt($this->handle, CURLOPT_TIMEOUT, ceil($timeout));
		} else {
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_timeout_msFound
			curl_setopt($this->handle, CURLOPT_TIMEOUT_MS, round($timeout * 1000));
		}

		if (is_int($options['connect_timeout']) || $this->version < self::CURL_7_16_2) {
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT, ceil($options['connect_timeout']));
		} else {
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.curlopt_connecttimeout_msFound
			curl_setopt($this->handle, CURLOPT_CONNECTTIMEOUT_MS, round($options['connect_timeout'] * 1000));
		}

		curl_setopt($this->handle, CURLOPT_URL, $url);
		curl_setopt($this->handle, CURLOPT_USERAGENT, $options['useragent']);
		if (!empty($headers)) {
			curl_setopt($this->handle, CURLOPT_HTTPHEADER, $headers);
		}

		if ($options['protocol_version'] === 1.1) {
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
		} else {
			curl_setopt($this->handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
		}

		if ($options['blocking'] === true) {
			curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, [$this, 'stream_headers']);
			curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, [$this, 'stream_body']);
			curl_setopt($this->handle, CURLOPT_BUFFERSIZE, Requests::BUFFER_SIZE);
		}
	}

	/**
	 * Process a response
	 *
	 * @param string $response Response data from the body
	 * @param array $options Request options
	 * @return string|false HTTP response data including headers. False if non-blocking.
	 * @throws \WpOrg\Requests\Exception If the request resulted in a cURL error.
	 */
	public function process_response($response, $options) {
		if ($options['blocking'] === false) {
			$fake_headers = '';
			$options['hooks']->dispatch('curl.after_request', [&$fake_headers]);
			return false;
		}

		if ($options['filename'] !== false && $this->stream_handle) {
			fclose($this->stream_handle);
			$this->headers = trim($this->headers);
		} else {
			$this->headers .= $response;
		}

		if (curl_errno($this->handle)) {
			$error = sprintf(
				'cURL error %s: %s',
				curl_errno($this->handle),
				curl_error($this->handle)
			);
			throw new Exception($error, 'curlerror', $this->handle);
		}

		$this->info = curl_getinfo($this->handle);

		$options['hooks']->dispatch('curl.after_request', [&$this->headers, &$this->info]);
		return $this->headers;
	}

	/**
	 * Collect the headers as they are received
	 *
	 * @param resource|\CurlHandle $handle cURL handle
	 * @param string $headers Header string
	 * @return integer Length of provided header
	 */
	public function stream_headers($handle, $headers) {
		// Why do we do this? cURL will send both the final response and any
		// interim responses, such as a 100 Continue. We don't need that.
		// (We may want to keep this somewhere just in case)
		if ($this->done_headers) {
			$this->headers      = '';
			$this->done_headers = false;
		}

		$this->headers .= $headers;

		if ($headers === "\r\n") {
			$this->done_headers = true;
		}

		return strlen($headers);
	}

	/**
	 * Collect data as it's received
	 *
	 * @since 1.6.1
	 *
	 * @param resource|\CurlHandle $handle cURL handle
	 * @param string $data Body data
	 * @return integer Length of provided data
	 */
	public function stream_body($handle, $data) {
		$this->hooks->dispatch('request.progress', [$data, $this->response_bytes, $this->response_byte_limit]);
		$data_length = strlen($data);

		// Are we limiting the response size?
		if ($this->response_byte_limit) {
			if ($this->response_bytes === $this->response_byte_limit) {
				// Already at maximum, move on
				return $data_length;
			}

			if (($this->response_bytes + $data_length) > $this->response_byte_limit) {
				// Limit the length
				$limited_length = ($this->response_byte_limit - $this->response_bytes);
				$data           = substr($data, 0, $limited_length);
			}
		}

		if ($this->stream_handle) {
			fwrite($this->stream_handle, $data);
		} else {
			$this->response_data .= $data;
		}

		$this->response_bytes += strlen($data);
		return $data_length;
	}

	/**
	 * Format a URL given GET data
	 *
	 * @param string       $url  Original URL.
	 * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query}
	 * @return string URL with data
	 */
	private static function format_get($url, $data) {
		if (!empty($data)) {
			$query     = '';
			$url_parts = parse_url($url);
			if (empty($url_parts['query'])) {
				$url_parts['query'] = '';
			} else {
				$query = $url_parts['query'];
			}

			$query .= '&' . http_build_query($data, '', '&');
			$query  = trim($query, '&');

			if (empty($url_parts['query'])) {
				$url .= '?' . $query;
			} else {
				$url = str_replace($url_parts['query'], $query, $url);
			}
		}

		return $url;
	}

	/**
	 * Self-test whether the transport can be used.
	 *
	 * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}.
	 *
	 * @codeCoverageIgnore
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return bool Whether the transport can be used.
	 */
	public static function test($capabilities = []) {
		if (!function_exists('curl_init') || !function_exists('curl_exec')) {
			return false;
		}

		// If needed, check that our installed curl version supports SSL
		if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) {
			$curl_version = curl_version();
			if (!(CURL_VERSION_SSL & $curl_version['features'])) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get the correct "Expect" header for the given request data.
	 *
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD.
	 * @return string The "Expect" header.
	 */
	private function get_expect_header($data) {
		if (!is_array($data)) {
			return strlen((string) $data) >= 1048576 ? '100-Continue' : '';
		}

		$bytesize = 0;
		$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($data));

		foreach ($iterator as $datum) {
			$bytesize += strlen((string) $datum);

			if ($bytesize >= 1048576) {
				return '100-Continue';
			}
		}

		return '';
	}
}
                                                                                          Transport/Fsockopen.php                                                                             0000644                 00000037317 15227642210 0011214 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * fsockopen HTTP transport
 *
 * @package Requests\Transport
 */

namespace WpOrg\Requests\Transport;

use WpOrg\Requests\Capability;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Port;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Ssl;
use WpOrg\Requests\Transport;
use WpOrg\Requests\Utility\CaseInsensitiveDictionary;
use WpOrg\Requests\Utility\InputValidator;

/**
 * fsockopen HTTP transport
 *
 * @package Requests\Transport
 */
final class Fsockopen implements Transport {
	/**
	 * Second to microsecond conversion
	 *
	 * @var integer
	 */
	const SECOND_IN_MICROSECONDS = 1000000;

	/**
	 * Raw HTTP data
	 *
	 * @var string
	 */
	public $headers = '';

	/**
	 * Stream metadata
	 *
	 * @var array Associative array of properties, see {@link https://www.php.net/stream_get_meta_data}
	 */
	public $info;

	/**
	 * What's the maximum number of bytes we should keep?
	 *
	 * @var int|bool Byte count, or false if no limit.
	 */
	private $max_bytes = false;

	/**
	 * Cache for received connection errors.
	 *
	 * @var string
	 */
	private $connect_error = '';

	/**
	 * Perform a request
	 *
	 * @param string|Stringable $url URL to request
	 * @param array $headers Associative array of request headers
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
	 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return string Raw HTTP result
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data parameter is not an array or string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 * @throws \WpOrg\Requests\Exception       On failure to connect to socket (`fsockopenerror`)
	 * @throws \WpOrg\Requests\Exception       On socket timeout (`timeout`)
	 */
	public function request($url, $headers = [], $data = [], $options = []) {
		if (InputValidator::is_string_or_stringable($url) === false) {
			throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url));
		}

		if (is_array($headers) === false) {
			throw InvalidArgument::create(2, '$headers', 'array', gettype($headers));
		}

		if (!is_array($data) && !is_string($data)) {
			if ($data === null) {
				$data = '';
			} else {
				throw InvalidArgument::create(3, '$data', 'array|string', gettype($data));
			}
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(4, '$options', 'array', gettype($options));
		}

		$options['hooks']->dispatch('fsockopen.before_request');

		$url_parts = parse_url($url);
		if (empty($url_parts)) {
			throw new Exception('Invalid URL.', 'invalidurl', $url);
		}

		$host                     = $url_parts['host'];
		$context                  = stream_context_create();
		$verifyname               = false;
		$case_insensitive_headers = new CaseInsensitiveDictionary($headers);

		// HTTPS support
		if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
			$remote_socket = 'ssl://' . $host;
			if (!isset($url_parts['port'])) {
				$url_parts['port'] = Port::HTTPS;
			}

			$context_options = [
				'verify_peer'       => true,
				'capture_peer_cert' => true,
			];
			$verifyname      = true;

			// SNI, if enabled (OpenSSL >=0.9.8j)
			// phpcs:ignore PHPCompatibility.Constants.NewConstants.openssl_tlsext_server_nameFound
			if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
				$context_options['SNI_enabled'] = true;
				if (isset($options['verifyname']) && $options['verifyname'] === false) {
					$context_options['SNI_enabled'] = false;
				}
			}

			if (isset($options['verify'])) {
				if ($options['verify'] === false) {
					$context_options['verify_peer']      = false;
					$context_options['verify_peer_name'] = false;
					$verifyname                          = false;
				} elseif (is_string($options['verify'])) {
					$context_options['cafile'] = $options['verify'];
				}
			}

			if (isset($options['verifyname']) && $options['verifyname'] === false) {
				$context_options['verify_peer_name'] = false;
				$verifyname                          = false;
			}

			// Handle the PHP 8.4 deprecation (PHP 9.0 removal) of the function signature we use for stream_context_set_option().
			// Ref: https://wiki.php.net/rfc/deprecate_functions_with_overloaded_signatures#stream_context_set_option
			if (function_exists('stream_context_set_options')) {
				// PHP 8.3+.
				// phpcs:ignore PHPCompatibility.FunctionUse.NewFunctions.stream_context_set_optionsFound
				stream_context_set_options($context, ['ssl' => $context_options]);
			} else {
				// PHP < 8.3.
				// phpcs:ignore PHPCompatibility.FunctionUse.OptionalToRequiredFunctionParameters
				stream_context_set_option($context, ['ssl' => $context_options]);
			}
		} else {
			$remote_socket = 'tcp://' . $host;
		}

		$this->max_bytes = $options['max_bytes'];

		if (!isset($url_parts['port'])) {
			$url_parts['port'] = Port::HTTP;
		}

		$remote_socket .= ':' . $url_parts['port'];

		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler
		set_error_handler([$this, 'connect_error_handler'], E_WARNING | E_NOTICE);

		$options['hooks']->dispatch('fsockopen.remote_socket', [&$remote_socket]);

		$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);

		restore_error_handler();

		if ($verifyname && !$this->verify_certificate_from_context($host, $context)) {
			throw new Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
		}

		if (!$socket) {
			if ($errno === 0) {
				// Connection issue
				throw new Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
			}

			throw new Exception($errstr, 'fsockopenerror', null, $errno);
		}

		$data_format = $options['data_format'];

		if ($data_format === 'query') {
			$path = self::format_get($url_parts, $data);
			$data = '';
		} else {
			$path = self::format_get($url_parts, []);
		}

		$options['hooks']->dispatch('fsockopen.remote_host_path', [&$path, $url]);

		$request_body = '';
		$out          = sprintf("%s %s HTTP/%.1F\r\n", $options['type'], $path, $options['protocol_version']);

		if ($options['type'] !== Requests::TRACE) {
			if (is_array($data)) {
				$request_body = http_build_query($data, '', '&');
			} else {
				$request_body = $data;
			}

			// Always include Content-length on POST requests to prevent
			// 411 errors from some servers when the body is empty.
			if (!empty($data) || $options['type'] === Requests::POST) {
				if (!isset($case_insensitive_headers['Content-Length'])) {
					$headers['Content-Length'] = strlen($request_body);
				}

				if (!isset($case_insensitive_headers['Content-Type'])) {
					$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
				}
			}
		}

		if (!isset($case_insensitive_headers['Host'])) {
			$out         .= sprintf('Host: %s', $url_parts['host']);
			$scheme_lower = strtolower($url_parts['scheme']);

			if (($scheme_lower === 'http' && $url_parts['port'] !== Port::HTTP) || ($scheme_lower === 'https' && $url_parts['port'] !== Port::HTTPS)) {
				$out .= ':' . $url_parts['port'];
			}

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

		if (!isset($case_insensitive_headers['User-Agent'])) {
			$out .= sprintf("User-Agent: %s\r\n", $options['useragent']);
		}

		$accept_encoding = $this->accept_encoding();
		if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) {
			$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding);
		}

		$headers = Requests::flatten($headers);

		if (!empty($headers)) {
			$out .= implode("\r\n", $headers) . "\r\n";
		}

		$options['hooks']->dispatch('fsockopen.after_headers', [&$out]);

		if (substr($out, -2) !== "\r\n") {
			$out .= "\r\n";
		}

		if (!isset($case_insensitive_headers['Connection'])) {
			$out .= "Connection: Close\r\n";
		}

		$out .= "\r\n" . $request_body;

		$options['hooks']->dispatch('fsockopen.before_send', [&$out]);

		fwrite($socket, $out);
		$options['hooks']->dispatch('fsockopen.after_send', [$out]);

		if (!$options['blocking']) {
			fclose($socket);
			$fake_headers = '';
			$options['hooks']->dispatch('fsockopen.after_request', [&$fake_headers]);
			return '';
		}

		$timeout_sec = (int) floor($options['timeout']);
		if ($timeout_sec === $options['timeout']) {
			$timeout_msec = 0;
		} else {
			$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
		}

		stream_set_timeout($socket, $timeout_sec, $timeout_msec);

		$response   = '';
		$body       = '';
		$headers    = '';
		$this->info = stream_get_meta_data($socket);
		$size       = 0;
		$doingbody  = false;
		$download   = false;
		if ($options['filename']) {
			// phpcs:ignore WordPress.PHP.NoSilencedErrors -- Silenced the PHP native warning in favour of throwing an exception.
			$download = @fopen($options['filename'], 'wb');
			if ($download === false) {
				$error = error_get_last();
				throw new Exception($error['message'], 'fopen');
			}
		}

		while (!feof($socket)) {
			$this->info = stream_get_meta_data($socket);
			if ($this->info['timed_out']) {
				throw new Exception('fsocket timed out', 'timeout');
			}

			$block = fread($socket, Requests::BUFFER_SIZE);
			if (!$doingbody) {
				$response .= $block;
				if (strpos($response, "\r\n\r\n")) {
					list($headers, $block) = explode("\r\n\r\n", $response, 2);
					$doingbody             = true;
				}
			}

			// Are we in body mode now?
			if ($doingbody) {
				$options['hooks']->dispatch('request.progress', [$block, $size, $this->max_bytes]);
				$data_length = strlen($block);
				if ($this->max_bytes) {
					// Have we already hit a limit?
					if ($size === $this->max_bytes) {
						continue;
					}

					if (($size + $data_length) > $this->max_bytes) {
						// Limit the length
						$limited_length = ($this->max_bytes - $size);
						$block          = substr($block, 0, $limited_length);
					}
				}

				$size += strlen($block);
				if ($download) {
					fwrite($download, $block);
				} else {
					$body .= $block;
				}
			}
		}

		$this->headers = $headers;

		if ($download) {
			fclose($download);
		} else {
			$this->headers .= "\r\n\r\n" . $body;
		}

		fclose($socket);

		$options['hooks']->dispatch('fsockopen.after_request', [&$this->headers, &$this->info]);
		return $this->headers;
	}

	/**
	 * Send multiple requests simultaneously
	 *
	 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()}
	 * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well)
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 */
	public function request_multiple($requests, $options) {
		// If you're not requesting, we can't get any responses ¯\_(ツ)_/¯
		if (empty($requests)) {
			return [];
		}

		if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
			throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(2, '$options', 'array', gettype($options));
		}

		$responses = [];
		$class     = get_class($this);
		foreach ($requests as $id => $request) {
			try {
				$handler        = new $class();
				$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);

				$request['options']['hooks']->dispatch('transport.internal.parse_response', [&$responses[$id], $request]);
			} catch (Exception $e) {
				$responses[$id] = $e;
			}

			if (!is_string($responses[$id])) {
				$request['options']['hooks']->dispatch('multiple.request.complete', [&$responses[$id], $id]);
			}
		}

		return $responses;
	}

	/**
	 * Retrieve the encodings we can accept
	 *
	 * @return string Accept-Encoding header value
	 */
	private static function accept_encoding() {
		$type = [];
		if (function_exists('gzinflate')) {
			$type[] = 'deflate;q=1.0';
		}

		if (function_exists('gzuncompress')) {
			$type[] = 'compress;q=0.5';
		}

		$type[] = 'gzip;q=0.5';

		return implode(', ', $type);
	}

	/**
	 * Format a URL given GET data
	 *
	 * @param array        $url_parts Array of URL parts as received from {@link https://www.php.net/parse_url}
	 * @param array|object $data Data to build query using, see {@link https://www.php.net/http_build_query}
	 * @return string URL with data
	 */
	private static function format_get($url_parts, $data) {
		if (!empty($data)) {
			if (empty($url_parts['query'])) {
				$url_parts['query'] = '';
			}

			$url_parts['query'] .= '&' . http_build_query($data, '', '&');
			$url_parts['query']  = trim($url_parts['query'], '&');
		}

		if (isset($url_parts['path'])) {
			if (isset($url_parts['query'])) {
				$get = $url_parts['path'] . '?' . $url_parts['query'];
			} else {
				$get = $url_parts['path'];
			}
		} else {
			$get = '/';
		}

		return $get;
	}

	/**
	 * Error handler for stream_socket_client()
	 *
	 * @param int $errno Error number (e.g. E_WARNING)
	 * @param string $errstr Error message
	 */
	public function connect_error_handler($errno, $errstr) {
		// Double-check we can handle it
		if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
			// Return false to indicate the default error handler should engage
			return false;
		}

		$this->connect_error .= $errstr . "\n";
		return true;
	}

	/**
	 * Verify the certificate against common name and subject alternative names
	 *
	 * Unfortunately, PHP doesn't check the certificate against the alternative
	 * names, leading things like 'https://www.github.com/' to be invalid.
	 * Instead
	 *
	 * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
	 *
	 * @param string $host Host name to verify against
	 * @param resource $context Stream context
	 * @return bool
	 *
	 * @throws \WpOrg\Requests\Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
	 * @throws \WpOrg\Requests\Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
	 */
	public function verify_certificate_from_context($host, $context) {
		$meta = stream_context_get_options($context);

		// If we don't have SSL options, then we couldn't make the connection at
		// all
		if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
			throw new Exception(rtrim($this->connect_error), 'ssl.connect_error');
		}

		$cert = openssl_x509_parse($meta['ssl']['peer_certificate']);

		return Ssl::verify_certificate($host, $cert);
	}

	/**
	 * Self-test whether the transport can be used.
	 *
	 * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}.
	 *
	 * @codeCoverageIgnore
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return bool Whether the transport can be used.
	 */
	public static function test($capabilities = []) {
		if (!function_exists('fsockopen')) {
			return false;
		}

		// If needed, check that streams support SSL
		if (isset($capabilities[Capability::SSL]) && $capabilities[Capability::SSL]) {
			if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) {
				return false;
			}
		}

		return true;
	}
}
                                                                                                                                                                                                                                                                                                                 Transport/error_log                                                                                 0000644                 00000002344 15227642210 0010461 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 16:15:33 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Curl.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Curl.php on line 25
[19-Jul-2026 05:17:57 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Fsockopen.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Fsockopen.php on line 25
[19-Jul-2026 11:58:19 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Curl.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Curl.php on line 25
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Fsockopen.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Transport/Fsockopen.php on line 25
                                                                                                                                                                                                                                                                                            Ipv6.php                                                                                            0000644                 00000013013 15227642210 0006100 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Class to validate and to work with IPv6 addresses
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Class to validate and to work with IPv6 addresses
 *
 * This was originally based on the PEAR class of the same name, but has been
 * entirely rewritten.
 *
 * @package Requests\Utilities
 */
final class Ipv6 {
	/**
	 * Uncompresses an IPv6 address
	 *
	 * RFC 4291 allows you to compress consecutive zero pieces in an address to
	 * '::'. This method expects a valid IPv6 address and expands the '::' to
	 * the required number of zero pieces.
	 *
	 * Example:  FF01::101   ->  FF01:0:0:0:0:0:0:101
	 *           ::1         ->  0:0:0:0:0:0:0:1
	 *
	 * @author Alexander Merz <alexander.merz@web.de>
	 * @author elfrink at introweb dot nl
	 * @author Josh Peck <jmp at joshpeck dot org>
	 * @copyright 2003-2005 The PHP Group
	 * @license https://opensource.org/licenses/bsd-license.php
	 *
	 * @param string|Stringable $ip An IPv6 address
	 * @return string The uncompressed IPv6 address
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object.
	 */
	public static function uncompress($ip) {
		if (InputValidator::is_string_or_stringable($ip) === false) {
			throw InvalidArgument::create(1, '$ip', 'string|Stringable', gettype($ip));
		}

		$ip = (string) $ip;

		if (substr_count($ip, '::') !== 1) {
			return $ip;
		}

		list($ip1, $ip2) = explode('::', $ip);
		$c1              = ($ip1 === '') ? -1 : substr_count($ip1, ':');
		$c2              = ($ip2 === '') ? -1 : substr_count($ip2, ':');

		if (strpos($ip2, '.') !== false) {
			$c2++;
		}

		if ($c1 === -1 && $c2 === -1) {
			// ::
			$ip = '0:0:0:0:0:0:0:0';
		} elseif ($c1 === -1) {
			// ::xxx
			$fill = str_repeat('0:', 7 - $c2);
			$ip   = str_replace('::', $fill, $ip);
		} elseif ($c2 === -1) {
			// xxx::
			$fill = str_repeat(':0', 7 - $c1);
			$ip   = str_replace('::', $fill, $ip);
		} else {
			// xxx::xxx
			$fill = ':' . str_repeat('0:', 6 - $c2 - $c1);
			$ip   = str_replace('::', $fill, $ip);
		}

		return $ip;
	}

	/**
	 * Compresses an IPv6 address
	 *
	 * RFC 4291 allows you to compress consecutive zero pieces in an address to
	 * '::'. This method expects a valid IPv6 address and compresses consecutive
	 * zero pieces to '::'.
	 *
	 * Example:  FF01:0:0:0:0:0:0:101   ->  FF01::101
	 *           0:0:0:0:0:0:0:1        ->  ::1
	 *
	 * @see \WpOrg\Requests\Ipv6::uncompress()
	 *
	 * @param string $ip An IPv6 address
	 * @return string The compressed IPv6 address
	 */
	public static function compress($ip) {
		// Prepare the IP to be compressed.
		// Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method.
		$ip       = self::uncompress($ip);
		$ip_parts = self::split_v6_v4($ip);

		// Replace all leading zeros
		$ip_parts[0] = preg_replace('/(^|:)0+([0-9])/', '\1\2', $ip_parts[0]);

		// Find bunches of zeros
		if (preg_match_all('/(?:^|:)(?:0(?::|$))+/', $ip_parts[0], $matches, PREG_OFFSET_CAPTURE)) {
			$max = 0;
			$pos = null;
			foreach ($matches[0] as $match) {
				if (strlen($match[0]) > $max) {
					$max = strlen($match[0]);
					$pos = $match[1];
				}
			}

			$ip_parts[0] = substr_replace($ip_parts[0], '::', $pos, $max);
		}

		if ($ip_parts[1] !== '') {
			return implode(':', $ip_parts);
		} else {
			return $ip_parts[0];
		}
	}

	/**
	 * Splits an IPv6 address into the IPv6 and IPv4 representation parts
	 *
	 * RFC 4291 allows you to represent the last two parts of an IPv6 address
	 * using the standard IPv4 representation
	 *
	 * Example:  0:0:0:0:0:0:13.1.68.3
	 *           0:0:0:0:0:FFFF:129.144.52.38
	 *
	 * @param string $ip An IPv6 address
	 * @return string[] [0] contains the IPv6 represented part, and [1] the IPv4 represented part
	 */
	private static function split_v6_v4($ip) {
		if (strpos($ip, '.') !== false) {
			$pos       = strrpos($ip, ':');
			$ipv6_part = substr($ip, 0, $pos);
			$ipv4_part = substr($ip, $pos + 1);
			return [$ipv6_part, $ipv4_part];
		} else {
			return [$ip, ''];
		}
	}

	/**
	 * Checks an IPv6 address
	 *
	 * Checks if the given IP is a valid IPv6 address
	 *
	 * @param string $ip An IPv6 address
	 * @return bool true if $ip is a valid IPv6 address
	 */
	public static function check_ipv6($ip) {
		// Note: Input validation is handled in the `uncompress()` method, which is the first call made in this method.
		$ip                = self::uncompress($ip);
		list($ipv6, $ipv4) = self::split_v6_v4($ip);
		$ipv6              = explode(':', $ipv6);
		$ipv4              = explode('.', $ipv4);
		if ((count($ipv6) === 8 && count($ipv4) === 1) || (count($ipv6) === 6 && count($ipv4) === 4)) {
			foreach ($ipv6 as $ipv6_part) {
				// The section can't be empty
				if ($ipv6_part === '') {
					return false;
				}

				// Nor can it be over four characters
				if (strlen($ipv6_part) > 4) {
					return false;
				}

				// Remove leading zeros (this is safe because of the above)
				$ipv6_part = ltrim($ipv6_part, '0');
				if ($ipv6_part === '') {
					$ipv6_part = '0';
				}

				// Check the value is valid
				$value = hexdec($ipv6_part);
				if (dechex($value) !== strtolower($ipv6_part) || $value < 0 || $value > 0xFFFF) {
					return false;
				}
			}

			if (count($ipv4) === 4) {
				foreach ($ipv4 as $ipv4_part) {
					$value = (int) $ipv4_part;
					if ((string) $value !== $ipv4_part || $value < 0 || $value > 0xFF) {
						return false;
					}
				}
			}

			return true;
		} else {
			return false;
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     error_log                                                                                           0000644                 00000001110 15227642210 0006453 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [15-Jul-2026 18:57:27 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\HookManager" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Hooks.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Hooks.php on line 19
[20-Jul-2026 07:51:30 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\HookManager" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Hooks.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Hooks.php on line 19
                                                                                                                                                                                                                                                                                                                                                                                                                                                        Proxy/Http.php                                                                                      0000644                 00000010171 15227642210 0007316 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * HTTP Proxy connection interface
 *
 * @package Requests\Proxy
 * @since   1.6
 */

namespace WpOrg\Requests\Proxy;

use WpOrg\Requests\Exception\ArgumentCount;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooks;
use WpOrg\Requests\Proxy;

/**
 * HTTP Proxy connection interface
 *
 * Provides a handler for connection via an HTTP proxy
 *
 * @package Requests\Proxy
 * @since   1.6
 */
final class Http implements Proxy {
	/**
	 * Proxy host and port
	 *
	 * Notation: "host:port" (eg 127.0.0.1:8080 or someproxy.com:3128)
	 *
	 * @var string
	 */
	public $proxy;

	/**
	 * Username
	 *
	 * @var string
	 */
	public $user;

	/**
	 * Password
	 *
	 * @var string
	 */
	public $pass;

	/**
	 * Do we need to authenticate? (ie username & password have been provided)
	 *
	 * @var boolean
	 */
	public $use_authentication;

	/**
	 * Constructor
	 *
	 * @since 1.6
	 *
	 * @param array|string|null $args Proxy as a string or an array of proxy, user and password.
	 *                                When passed as an array, must have exactly one (proxy)
	 *                                or three elements (proxy, user, password).
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array, a string or null.
	 * @throws \WpOrg\Requests\Exception\ArgumentCount On incorrect number of arguments (`proxyhttpbadargs`)
	 */
	public function __construct($args = null) {
		if (is_string($args)) {
			$this->proxy = $args;
		} elseif (is_array($args)) {
			if (count($args) === 1) {
				list($this->proxy) = $args;
			} elseif (count($args) === 3) {
				list($this->proxy, $this->user, $this->pass) = $args;
				$this->use_authentication                    = true;
			} else {
				throw ArgumentCount::create(
					'an array with exactly one element or exactly three elements',
					count($args),
					'proxyhttpbadargs'
				);
			}
		} elseif ($args !== null) {
			throw InvalidArgument::create(1, '$args', 'array|string|null', gettype($args));
		}
	}

	/**
	 * Register the necessary callbacks
	 *
	 * @since 1.6
	 * @see \WpOrg\Requests\Proxy\Http::curl_before_send()
	 * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_socket()
	 * @see \WpOrg\Requests\Proxy\Http::fsockopen_remote_host_path()
	 * @see \WpOrg\Requests\Proxy\Http::fsockopen_header()
	 * @param \WpOrg\Requests\Hooks $hooks Hook system
	 */
	public function register(Hooks $hooks) {
		$hooks->register('curl.before_send', [$this, 'curl_before_send']);

		$hooks->register('fsockopen.remote_socket', [$this, 'fsockopen_remote_socket']);
		$hooks->register('fsockopen.remote_host_path', [$this, 'fsockopen_remote_host_path']);
		if ($this->use_authentication) {
			$hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']);
		}
	}

	/**
	 * Set cURL parameters before the data is sent
	 *
	 * @since 1.6
	 * @param resource|\CurlHandle $handle cURL handle
	 */
	public function curl_before_send(&$handle) {
		curl_setopt($handle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
		curl_setopt($handle, CURLOPT_PROXY, $this->proxy);

		if ($this->use_authentication) {
			curl_setopt($handle, CURLOPT_PROXYAUTH, CURLAUTH_ANY);
			curl_setopt($handle, CURLOPT_PROXYUSERPWD, $this->get_auth_string());
		}
	}

	/**
	 * Alter remote socket information before opening socket connection
	 *
	 * @since 1.6
	 * @param string $remote_socket Socket connection string
	 */
	public function fsockopen_remote_socket(&$remote_socket) {
		$remote_socket = $this->proxy;
	}

	/**
	 * Alter remote path before getting stream data
	 *
	 * @since 1.6
	 * @param string $path Path to send in HTTP request string ("GET ...")
	 * @param string $url Full URL we're requesting
	 */
	public function fsockopen_remote_host_path(&$path, $url) {
		$path = $url;
	}

	/**
	 * Add extra headers to the request before sending
	 *
	 * @since 1.6
	 * @param string $out HTTP header string
	 */
	public function fsockopen_header(&$out) {
		$out .= sprintf("Proxy-Authorization: Basic %s\r\n", base64_encode($this->get_auth_string()));
	}

	/**
	 * Get the authentication string (user:pass)
	 *
	 * @since 1.6
	 * @return string
	 */
	public function get_auth_string() {
		return $this->user . ':' . $this->pass;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                       Proxy/error_log                                                                                     0000644                 00000001120 15227642210 0007575 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [18-Jul-2026 14:15:39 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Proxy" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Proxy/Http.php:24
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Proxy/Http.php on line 24
[19-Jul-2026 19:58:29 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Proxy" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Proxy/Http.php:24
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Proxy/Http.php on line 24
                                                                                                                                                                                                                                                                                                                                                                                                                                                Exception.php                                                                                       0000644                 00000002132 15227642210 0007212 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for HTTP requests
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests;

use Exception as PHPException;

/**
 * Exception for HTTP requests
 *
 * @package Requests\Exceptions
 */
class Exception extends PHPException {
	/**
	 * Type of exception
	 *
	 * @var string
	 */
	protected $type;

	/**
	 * Data associated with the exception
	 *
	 * @var mixed
	 */
	protected $data;

	/**
	 * Create a new exception
	 *
	 * @param string $message Exception message
	 * @param string $type Exception type
	 * @param mixed $data Associated data
	 * @param integer $code Exception numerical code, if applicable
	 */
	public function __construct($message, $type, $data = null, $code = 0) {
		parent::__construct($message, $code);

		$this->type = $type;
		$this->data = $data;
	}

	/**
	 * Like {@see \Exception::getCode()}, but a string code.
	 *
	 * @codeCoverageIgnore
	 * @return string
	 */
	public function getType() {
		return $this->type;
	}

	/**
	 * Gives any relevant data
	 *
	 * @codeCoverageIgnore
	 * @return mixed
	 */
	public function getData() {
		return $this->data;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                      Port.php                                                                                            0000644                 00000002741 15227642210 0006206 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Port utilities for Requests
 *
 * @package Requests\Utilities
 * @since   2.0.0
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;

/**
 * Find the correct port depending on the Request type.
 *
 * @package Requests\Utilities
 * @since   2.0.0
 */
final class Port {

	/**
	 * Port to use with Acap requests.
	 *
	 * @var int
	 */
	const ACAP = 674;

	/**
	 * Port to use with Dictionary requests.
	 *
	 * @var int
	 */
	const DICT = 2628;

	/**
	 * Port to use with HTTP requests.
	 *
	 * @var int
	 */
	const HTTP = 80;

	/**
	 * Port to use with HTTP over SSL requests.
	 *
	 * @var int
	 */
	const HTTPS = 443;

	/**
	 * Retrieve the port number to use.
	 *
	 * @param string $type Request type.
	 *                     The following requests types are supported:
	 *                     'acap', 'dict', 'http' and 'https'.
	 *
	 * @return int
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When a non-string input has been passed.
	 * @throws \WpOrg\Requests\Exception                 When a non-supported port is requested ('portnotsupported').
	 */
	public static function get($type) {
		if (!is_string($type)) {
			throw InvalidArgument::create(1, '$type', 'string', gettype($type));
		}

		$type = strtoupper($type);
		if (!defined("self::{$type}")) {
			$message = sprintf('Invalid port type (%s) passed', $type);
			throw new Exception($message, 'portnotsupported');
		}

		return constant("self::{$type}");
	}
}
                               Iri.php                                                                                             0000644                 00000072036 15227642210 0006011 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * IRI parser/serialiser/normaliser
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Ipv6;
use WpOrg\Requests\Port;
use WpOrg\Requests\Utility\InputValidator;

/**
 * IRI parser/serialiser/normaliser
 *
 * Copyright (c) 2007-2010, Geoffrey Sneddon and Steve Minutillo.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright notice,
 *       this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *       this list of conditions and the following disclaimer in the documentation
 *       and/or other materials provided with the distribution.
 *
 *  * Neither the name of the SimplePie Team nor the names of its contributors
 *       may be used to endorse or promote products derived from this software
 *       without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * @package Requests\Utilities
 * @author Geoffrey Sneddon
 * @author Steve Minutillo
 * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo
 * @license https://opensource.org/licenses/bsd-license.php
 * @link http://hg.gsnedders.com/iri/
 *
 * @property string $iri IRI we're working with
 * @property-read string $uri IRI in URI form, {@see \WpOrg\Requests\Iri::to_uri()}
 * @property string $scheme Scheme part of the IRI
 * @property string $authority Authority part, formatted for a URI (userinfo + host + port)
 * @property string $iauthority Authority part of the IRI (userinfo + host + port)
 * @property string $userinfo Userinfo part, formatted for a URI (after '://' and before '@')
 * @property string $iuserinfo Userinfo part of the IRI (after '://' and before '@')
 * @property string $host Host part, formatted for a URI
 * @property string $ihost Host part of the IRI
 * @property string $port Port part of the IRI (after ':')
 * @property string $path Path part, formatted for a URI (after first '/')
 * @property string $ipath Path part of the IRI (after first '/')
 * @property string $query Query part, formatted for a URI (after '?')
 * @property string $iquery Query part of the IRI (after '?')
 * @property string $fragment Fragment, formatted for a URI (after '#')
 * @property string $ifragment Fragment part of the IRI (after '#')
 */
class Iri {
	/**
	 * Scheme
	 *
	 * @var string|null
	 */
	protected $scheme = null;

	/**
	 * User Information
	 *
	 * @var string|null
	 */
	protected $iuserinfo = null;

	/**
	 * ihost
	 *
	 * @var string|null
	 */
	protected $ihost = null;

	/**
	 * Port
	 *
	 * @var string|null
	 */
	protected $port = null;

	/**
	 * ipath
	 *
	 * @var string
	 */
	protected $ipath = '';

	/**
	 * iquery
	 *
	 * @var string|null
	 */
	protected $iquery = null;

	/**
	 * ifragment|null
	 *
	 * @var string
	 */
	protected $ifragment = null;

	/**
	 * Normalization database
	 *
	 * Each key is the scheme, each value is an array with each key as the IRI
	 * part and value as the default value for that part.
	 *
	 * @var array
	 */
	protected $normalization = array(
		'acap' => array(
			'port' => Port::ACAP,
		),
		'dict' => array(
			'port' => Port::DICT,
		),
		'file' => array(
			'ihost' => 'localhost',
		),
		'http' => array(
			'port' => Port::HTTP,
		),
		'https' => array(
			'port' => Port::HTTPS,
		),
	);

	/**
	 * Return the entire IRI when you try and read the object as a string
	 *
	 * @return string
	 */
	public function __toString() {
		return $this->get_iri();
	}

	/**
	 * Overload __set() to provide access via properties
	 *
	 * @param string $name Property name
	 * @param mixed $value Property value
	 */
	public function __set($name, $value) {
		if (method_exists($this, 'set_' . $name)) {
			call_user_func(array($this, 'set_' . $name), $value);
		}
		elseif (
			   $name === 'iauthority'
			|| $name === 'iuserinfo'
			|| $name === 'ihost'
			|| $name === 'ipath'
			|| $name === 'iquery'
			|| $name === 'ifragment'
		) {
			call_user_func(array($this, 'set_' . substr($name, 1)), $value);
		}
	}

	/**
	 * Overload __get() to provide access via properties
	 *
	 * @param string $name Property name
	 * @return mixed
	 */
	public function __get($name) {
		// isset() returns false for null, we don't want to do that
		// Also why we use array_key_exists below instead of isset()
		$props = get_object_vars($this);

		if (
			$name === 'iri' ||
			$name === 'uri' ||
			$name === 'iauthority' ||
			$name === 'authority'
		) {
			$method = 'get_' . $name;
			$return = $this->$method();
		}
		elseif (array_key_exists($name, $props)) {
			$return = $this->$name;
		}
		// host -> ihost
		elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) {
			$name = $prop;
			$return = $this->$prop;
		}
		// ischeme -> scheme
		elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) {
			$name = $prop;
			$return = $this->$prop;
		}
		else {
			trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE);
			$return = null;
		}

		if ($return === null && isset($this->scheme, $this->normalization[$this->scheme][$name])) {
			return $this->normalization[$this->scheme][$name];
		}
		else {
			return $return;
		}
	}

	/**
	 * Overload __isset() to provide access via properties
	 *
	 * @param string $name Property name
	 * @return bool
	 */
	public function __isset($name) {
		return (method_exists($this, 'get_' . $name) || isset($this->$name));
	}

	/**
	 * Overload __unset() to provide access via properties
	 *
	 * @param string $name Property name
	 */
	public function __unset($name) {
		if (method_exists($this, 'set_' . $name)) {
			call_user_func(array($this, 'set_' . $name), '');
		}
	}

	/**
	 * Create a new IRI object, from a specified string
	 *
	 * @param string|Stringable|null $iri
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $iri argument is not a string, Stringable or null.
	 */
	public function __construct($iri = null) {
		if ($iri !== null && InputValidator::is_string_or_stringable($iri) === false) {
			throw InvalidArgument::create(1, '$iri', 'string|Stringable|null', gettype($iri));
		}

		$this->set_iri($iri);
	}

	/**
	 * Create a new IRI object by resolving a relative IRI
	 *
	 * Returns false if $base is not absolute, otherwise an IRI.
	 *
	 * @param \WpOrg\Requests\Iri|string $base (Absolute) Base IRI
	 * @param \WpOrg\Requests\Iri|string $relative Relative IRI
	 * @return \WpOrg\Requests\Iri|false
	 */
	public static function absolutize($base, $relative) {
		if (!($relative instanceof self)) {
			$relative = new self($relative);
		}
		if (!$relative->is_valid()) {
			return false;
		}
		elseif ($relative->scheme !== null) {
			return clone $relative;
		}

		if (!($base instanceof self)) {
			$base = new self($base);
		}
		if ($base->scheme === null || !$base->is_valid()) {
			return false;
		}

		if ($relative->get_iri() !== '') {
			if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) {
				$target = clone $relative;
				$target->scheme = $base->scheme;
			}
			else {
				$target = new self;
				$target->scheme = $base->scheme;
				$target->iuserinfo = $base->iuserinfo;
				$target->ihost = $base->ihost;
				$target->port = $base->port;
				if ($relative->ipath !== '') {
					if ($relative->ipath[0] === '/') {
						$target->ipath = $relative->ipath;
					}
					elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') {
						$target->ipath = '/' . $relative->ipath;
					}
					elseif (($last_segment = strrpos($base->ipath, '/')) !== false) {
						$target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath;
					}
					else {
						$target->ipath = $relative->ipath;
					}
					$target->ipath = $target->remove_dot_segments($target->ipath);
					$target->iquery = $relative->iquery;
				}
				else {
					$target->ipath = $base->ipath;
					if ($relative->iquery !== null) {
						$target->iquery = $relative->iquery;
					}
					elseif ($base->iquery !== null) {
						$target->iquery = $base->iquery;
					}
				}
				$target->ifragment = $relative->ifragment;
			}
		}
		else {
			$target = clone $base;
			$target->ifragment = null;
		}
		$target->scheme_normalization();
		return $target;
	}

	/**
	 * Parse an IRI into scheme/authority/path/query/fragment segments
	 *
	 * @param string $iri
	 * @return array
	 */
	protected function parse_iri($iri) {
		$iri = trim($iri, "\x20\x09\x0A\x0C\x0D");
		$has_match = preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match);
		if (!$has_match) {
			throw new Exception('Cannot parse supplied IRI', 'iri.cannot_parse', $iri);
		}

		if ($match[1] === '') {
			$match['scheme'] = null;
		}
		if (!isset($match[3]) || $match[3] === '') {
			$match['authority'] = null;
		}
		if (!isset($match[5])) {
			$match['path'] = '';
		}
		if (!isset($match[6]) || $match[6] === '') {
			$match['query'] = null;
		}
		if (!isset($match[8]) || $match[8] === '') {
			$match['fragment'] = null;
		}
		return $match;
	}

	/**
	 * Remove dot segments from a path
	 *
	 * @param string $input
	 * @return string
	 */
	protected function remove_dot_segments($input) {
		$output = '';
		while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') {
			// A: If the input buffer begins with a prefix of "../" or "./",
			// then remove that prefix from the input buffer; otherwise,
			if (strpos($input, '../') === 0) {
				$input = substr($input, 3);
			}
			elseif (strpos($input, './') === 0) {
				$input = substr($input, 2);
			}
			// B: if the input buffer begins with a prefix of "/./" or "/.",
			// where "." is a complete path segment, then replace that prefix
			// with "/" in the input buffer; otherwise,
			elseif (strpos($input, '/./') === 0) {
				$input = substr($input, 2);
			}
			elseif ($input === '/.') {
				$input = '/';
			}
			// C: if the input buffer begins with a prefix of "/../" or "/..",
			// where ".." is a complete path segment, then replace that prefix
			// with "/" in the input buffer and remove the last segment and its
			// preceding "/" (if any) from the output buffer; otherwise,
			elseif (strpos($input, '/../') === 0) {
				$input = substr($input, 3);
				$output = substr_replace($output, '', (strrpos($output, '/') ?: 0));
			}
			elseif ($input === '/..') {
				$input = '/';
				$output = substr_replace($output, '', (strrpos($output, '/') ?: 0));
			}
			// D: if the input buffer consists only of "." or "..", then remove
			// that from the input buffer; otherwise,
			elseif ($input === '.' || $input === '..') {
				$input = '';
			}
			// E: move the first path segment in the input buffer to the end of
			// the output buffer, including the initial "/" character (if any)
			// and any subsequent characters up to, but not including, the next
			// "/" character or the end of the input buffer
			elseif (($pos = strpos($input, '/', 1)) !== false) {
				$output .= substr($input, 0, $pos);
				$input = substr_replace($input, '', 0, $pos);
			}
			else {
				$output .= $input;
				$input = '';
			}
		}
		return $output . $input;
	}

	/**
	 * Replace invalid character with percent encoding
	 *
	 * @param string $text Input string
	 * @param string $extra_chars Valid characters not in iunreserved or
	 *                            iprivate (this is ASCII-only)
	 * @param bool $iprivate Allow iprivate
	 * @return string
	 */
	protected function replace_invalid_with_pct_encoding($text, $extra_chars, $iprivate = false) {
		// Normalize as many pct-encoded sections as possible
		$text = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array($this, 'remove_iunreserved_percent_encoded'), $text);

		// Replace invalid percent characters
		$text = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $text);

		// Add unreserved and % to $extra_chars (the latter is safe because all
		// pct-encoded sections are now valid).
		$extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%';

		// Now replace any bytes that aren't allowed with their pct-encoded versions
		$position = 0;
		$strlen = strlen($text);
		while (($position += strspn($text, $extra_chars, $position)) < $strlen) {
			$value = ord($text[$position]);

			// Start position
			$start = $position;

			// By default we are valid
			$valid = true;

			// No one byte sequences are valid due to the while.
			// Two byte sequence:
			if (($value & 0xE0) === 0xC0) {
				$character = ($value & 0x1F) << 6;
				$length = 2;
				$remaining = 1;
			}
			// Three byte sequence:
			elseif (($value & 0xF0) === 0xE0) {
				$character = ($value & 0x0F) << 12;
				$length = 3;
				$remaining = 2;
			}
			// Four byte sequence:
			elseif (($value & 0xF8) === 0xF0) {
				$character = ($value & 0x07) << 18;
				$length = 4;
				$remaining = 3;
			}
			// Invalid byte:
			else {
				$valid = false;
				$length = 1;
				$remaining = 0;
			}

			if ($remaining) {
				if ($position + $length <= $strlen) {
					for ($position++; $remaining; $position++) {
						$value = ord($text[$position]);

						// Check that the byte is valid, then add it to the character:
						if (($value & 0xC0) === 0x80) {
							$character |= ($value & 0x3F) << (--$remaining * 6);
						}
						// If it is invalid, count the sequence as invalid and reprocess the current byte:
						else {
							$valid = false;
							$position--;
							break;
						}
					}
				}
				else {
					$position = $strlen - 1;
					$valid = false;
				}
			}

			// Percent encode anything invalid or not in ucschar
			if (
				// Invalid sequences
				!$valid
				// Non-shortest form sequences are invalid
				|| $length > 1 && $character <= 0x7F
				|| $length > 2 && $character <= 0x7FF
				|| $length > 3 && $character <= 0xFFFF
				// Outside of range of ucschar codepoints
				// Noncharacters
				|| ($character & 0xFFFE) === 0xFFFE
				|| $character >= 0xFDD0 && $character <= 0xFDEF
				|| (
					// Everything else not in ucschar
					   $character > 0xD7FF && $character < 0xF900
					|| $character < 0xA0
					|| $character > 0xEFFFD
				)
				&& (
					// Everything not in iprivate, if it applies
					   !$iprivate
					|| $character < 0xE000
					|| $character > 0x10FFFD
				)
			) {
				// If we were a character, pretend we weren't, but rather an error.
				if ($valid) {
					$position--;
				}

				for ($j = $start; $j <= $position; $j++) {
					$text = substr_replace($text, sprintf('%%%02X', ord($text[$j])), $j, 1);
					$j += 2;
					$position += 2;
					$strlen += 2;
				}
			}
		}

		return $text;
	}

	/**
	 * Callback function for preg_replace_callback.
	 *
	 * Removes sequences of percent encoded bytes that represent UTF-8
	 * encoded characters in iunreserved
	 *
	 * @param array $regex_match PCRE match
	 * @return string Replacement
	 */
	protected function remove_iunreserved_percent_encoded($regex_match) {
		// As we just have valid percent encoded sequences we can just explode
		// and ignore the first member of the returned array (an empty string).
		$bytes = explode('%', $regex_match[0]);

		// Initialize the new string (this is what will be returned) and that
		// there are no bytes remaining in the current sequence (unsurprising
		// at the first byte!).
		$string = '';
		$remaining = 0;

		// Loop over each and every byte, and set $value to its value
		for ($i = 1, $len = count($bytes); $i < $len; $i++) {
			$value = hexdec($bytes[$i]);

			// If we're the first byte of sequence:
			if (!$remaining) {
				// Start position
				$start = $i;

				// By default we are valid
				$valid = true;

				// One byte sequence:
				if ($value <= 0x7F) {
					$character = $value;
					$length = 1;
				}
				// Two byte sequence:
				elseif (($value & 0xE0) === 0xC0) {
					$character = ($value & 0x1F) << 6;
					$length = 2;
					$remaining = 1;
				}
				// Three byte sequence:
				elseif (($value & 0xF0) === 0xE0) {
					$character = ($value & 0x0F) << 12;
					$length = 3;
					$remaining = 2;
				}
				// Four byte sequence:
				elseif (($value & 0xF8) === 0xF0) {
					$character = ($value & 0x07) << 18;
					$length = 4;
					$remaining = 3;
				}
				// Invalid byte:
				else {
					$valid = false;
					$remaining = 0;
				}
			}
			// Continuation byte:
			else {
				// Check that the byte is valid, then add it to the character:
				if (($value & 0xC0) === 0x80) {
					$remaining--;
					$character |= ($value & 0x3F) << ($remaining * 6);
				}
				// If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence:
				else {
					$valid = false;
					$remaining = 0;
					$i--;
				}
			}

			// If we've reached the end of the current byte sequence, append it to Unicode::$data
			if (!$remaining) {
				// Percent encode anything invalid or not in iunreserved
				if (
					// Invalid sequences
					!$valid
					// Non-shortest form sequences are invalid
					|| $length > 1 && $character <= 0x7F
					|| $length > 2 && $character <= 0x7FF
					|| $length > 3 && $character <= 0xFFFF
					// Outside of range of iunreserved codepoints
					|| $character < 0x2D
					|| $character > 0xEFFFD
					// Noncharacters
					|| ($character & 0xFFFE) === 0xFFFE
					|| $character >= 0xFDD0 && $character <= 0xFDEF
					// Everything else not in iunreserved (this is all BMP)
					|| $character === 0x2F
					|| $character > 0x39 && $character < 0x41
					|| $character > 0x5A && $character < 0x61
					|| $character > 0x7A && $character < 0x7E
					|| $character > 0x7E && $character < 0xA0
					|| $character > 0xD7FF && $character < 0xF900
				) {
					for ($j = $start; $j <= $i; $j++) {
						$string .= '%' . strtoupper($bytes[$j]);
					}
				}
				else {
					for ($j = $start; $j <= $i; $j++) {
						$string .= chr(hexdec($bytes[$j]));
					}
				}
			}
		}

		// If we have any bytes left over they are invalid (i.e., we are
		// mid-way through a multi-byte sequence)
		if ($remaining) {
			for ($j = $start; $j < $len; $j++) {
				$string .= '%' . strtoupper($bytes[$j]);
			}
		}

		return $string;
	}

	protected function scheme_normalization() {
		if (isset($this->scheme, $this->normalization[$this->scheme])) {
			if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) {
				$this->iuserinfo = null;
			}
			if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) {
				$this->ihost = null;
			}
			if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) {
				$this->port = null;
			}
			if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) {
				$this->ipath = '';
			}
			if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) {
				$this->iquery = null;
			}
			if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) {
				$this->ifragment = null;
			}
		}
		if (isset($this->ihost) && empty($this->ipath)) {
			$this->ipath = '/';
		}
	}

	/**
	 * Check if the object represents a valid IRI. This needs to be done on each
	 * call as some things change depending on another part of the IRI.
	 *
	 * @return bool
	 */
	public function is_valid() {
		$isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null;
		if ($this->ipath !== '' &&
			(
				$isauthority && $this->ipath[0] !== '/' ||
				(
					$this->scheme === null &&
					!$isauthority &&
					strpos($this->ipath, ':') !== false &&
					(strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/'))
				)
			)
		) {
			return false;
		}

		return true;
	}

	public function __wakeup() {
		$class_props = get_class_vars( __CLASS__ );
		$string_props = array( 'scheme', 'iuserinfo', 'ihost', 'port', 'ipath', 'iquery', 'ifragment' );
		$array_props = array( 'normalization' );
		foreach ( $class_props as $prop => $default_value ) {
			if ( in_array( $prop, $string_props, true ) && ! is_string( $this->$prop ) ) {
				throw new UnexpectedValueException();
			} elseif ( in_array( $prop, $array_props, true ) && ! is_array( $this->$prop ) ) {
				throw new UnexpectedValueException();
			}
			$this->$prop = null;
		}
	}

	/**
	 * Set the entire IRI. Returns true on success, false on failure (if there
	 * are any invalid characters).
	 *
	 * @param string $iri
	 * @return bool
	 */
	protected function set_iri($iri) {
		static $cache;
		if (!$cache) {
			$cache = array();
		}

		if ($iri === null) {
			return true;
		}

		$iri = (string) $iri;

		if (isset($cache[$iri])) {
			list($this->scheme,
				 $this->iuserinfo,
				 $this->ihost,
				 $this->port,
				 $this->ipath,
				 $this->iquery,
				 $this->ifragment,
				 $return) = $cache[$iri];
			return $return;
		}

		$parsed = $this->parse_iri($iri);

		$return = $this->set_scheme($parsed['scheme'])
			&& $this->set_authority($parsed['authority'])
			&& $this->set_path($parsed['path'])
			&& $this->set_query($parsed['query'])
			&& $this->set_fragment($parsed['fragment']);

		$cache[$iri] = array($this->scheme,
							 $this->iuserinfo,
							 $this->ihost,
							 $this->port,
							 $this->ipath,
							 $this->iquery,
							 $this->ifragment,
							 $return);
		return $return;
	}

	/**
	 * Set the scheme. Returns true on success, false on failure (if there are
	 * any invalid characters).
	 *
	 * @param string $scheme
	 * @return bool
	 */
	protected function set_scheme($scheme) {
		if ($scheme === null) {
			$this->scheme = null;
		}
		elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) {
			$this->scheme = null;
			return false;
		}
		else {
			$this->scheme = strtolower($scheme);
		}
		return true;
	}

	/**
	 * Set the authority. Returns true on success, false on failure (if there are
	 * any invalid characters).
	 *
	 * @param string $authority
	 * @return bool
	 */
	protected function set_authority($authority) {
		static $cache;
		if (!$cache) {
			$cache = array();
		}

		if ($authority === null) {
			$this->iuserinfo = null;
			$this->ihost = null;
			$this->port = null;
			return true;
		}
		if (isset($cache[$authority])) {
			list($this->iuserinfo,
				 $this->ihost,
				 $this->port,
				 $return) = $cache[$authority];

			return $return;
		}

		$remaining = $authority;
		if (($iuserinfo_end = strrpos($remaining, '@')) !== false) {
			$iuserinfo = substr($remaining, 0, $iuserinfo_end);
			$remaining = substr($remaining, $iuserinfo_end + 1);
		}
		else {
			$iuserinfo = null;
		}

		if (($port_start = strpos($remaining, ':', (strpos($remaining, ']') ?: 0))) !== false) {
			$port = substr($remaining, $port_start + 1);
			if ($port === false || $port === '') {
				$port = null;
			}
			$remaining = substr($remaining, 0, $port_start);
		}
		else {
			$port = null;
		}

		$return = $this->set_userinfo($iuserinfo) &&
				  $this->set_host($remaining) &&
				  $this->set_port($port);

		$cache[$authority] = array($this->iuserinfo,
								   $this->ihost,
								   $this->port,
								   $return);

		return $return;
	}

	/**
	 * Set the iuserinfo.
	 *
	 * @param string $iuserinfo
	 * @return bool
	 */
	protected function set_userinfo($iuserinfo) {
		if ($iuserinfo === null) {
			$this->iuserinfo = null;
		}
		else {
			$this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
			$this->scheme_normalization();
		}

		return true;
	}

	/**
	 * Set the ihost. Returns true on success, false on failure (if there are
	 * any invalid characters).
	 *
	 * @param string $ihost
	 * @return bool
	 */
	protected function set_host($ihost) {
		if ($ihost === null) {
			$this->ihost = null;
			return true;
		}
		if (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') {
			if (Ipv6::check_ipv6(substr($ihost, 1, -1))) {
				$this->ihost = '[' . Ipv6::compress(substr($ihost, 1, -1)) . ']';
			}
			else {
				$this->ihost = null;
				return false;
			}
		}
		else {
			$ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');

			// Lowercase, but ignore pct-encoded sections (as they should
			// remain uppercase). This must be done after the previous step
			// as that can add unescaped characters.
			$position = 0;
			$strlen = strlen($ihost);
			while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) {
				if ($ihost[$position] === '%') {
					$position += 3;
				}
				else {
					$ihost[$position] = strtolower($ihost[$position]);
					$position++;
				}
			}

			$this->ihost = $ihost;
		}

		$this->scheme_normalization();

		return true;
	}

	/**
	 * Set the port. Returns true on success, false on failure (if there are
	 * any invalid characters).
	 *
	 * @param string $port
	 * @return bool
	 */
	protected function set_port($port) {
		if ($port === null) {
			$this->port = null;
			return true;
		}

		if (strspn($port, '0123456789') === strlen($port)) {
			$this->port = (int) $port;
			$this->scheme_normalization();
			return true;
		}

		$this->port = null;
		return false;
	}

	/**
	 * Set the ipath.
	 *
	 * @param string $ipath
	 * @return bool
	 */
	protected function set_path($ipath) {
		static $cache;
		if (!$cache) {
			$cache = array();
		}

		$ipath = (string) $ipath;

		if (isset($cache[$ipath])) {
			$this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
		}
		else {
			$valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
			$removed = $this->remove_dot_segments($valid);

			$cache[$ipath] = array($valid, $removed);
			$this->ipath = ($this->scheme !== null) ? $removed : $valid;
		}
		$this->scheme_normalization();
		return true;
	}

	/**
	 * Set the iquery.
	 *
	 * @param string $iquery
	 * @return bool
	 */
	protected function set_query($iquery) {
		if ($iquery === null) {
			$this->iquery = null;
		}
		else {
			$this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
			$this->scheme_normalization();
		}
		return true;
	}

	/**
	 * Set the ifragment.
	 *
	 * @param string $ifragment
	 * @return bool
	 */
	protected function set_fragment($ifragment) {
		if ($ifragment === null) {
			$this->ifragment = null;
		}
		else {
			$this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
			$this->scheme_normalization();
		}
		return true;
	}

	/**
	 * Convert an IRI to a URI (or parts thereof)
	 *
	 * @param string|bool $iri IRI to convert (or false from {@see \WpOrg\Requests\Iri::get_iri()})
	 * @return string|false URI if IRI is valid, false otherwise.
	 */
	protected function to_uri($iri) {
		if (!is_string($iri)) {
			return false;
		}

		static $non_ascii;
		if (!$non_ascii) {
			$non_ascii = implode('', range("\x80", "\xFF"));
		}

		$position = 0;
		$strlen = strlen($iri);
		while (($position += strcspn($iri, $non_ascii, $position)) < $strlen) {
			$iri = substr_replace($iri, sprintf('%%%02X', ord($iri[$position])), $position, 1);
			$position += 3;
			$strlen += 2;
		}

		return $iri;
	}

	/**
	 * Get the complete IRI
	 *
	 * @return string|false
	 */
	protected function get_iri() {
		if (!$this->is_valid()) {
			return false;
		}

		$iri = '';
		if ($this->scheme !== null) {
			$iri .= $this->scheme . ':';
		}
		if (($iauthority = $this->get_iauthority()) !== null) {
			$iri .= '//' . $iauthority;
		}
		$iri .= $this->ipath;
		if ($this->iquery !== null) {
			$iri .= '?' . $this->iquery;
		}
		if ($this->ifragment !== null) {
			$iri .= '#' . $this->ifragment;
		}

		return $iri;
	}

	/**
	 * Get the complete URI
	 *
	 * @return string
	 */
	protected function get_uri() {
		return $this->to_uri($this->get_iri());
	}

	/**
	 * Get the complete iauthority
	 *
	 * @return string|null
	 */
	protected function get_iauthority() {
		if ($this->iuserinfo === null && $this->ihost === null && $this->port === null) {
			return null;
		}

		$iauthority = '';
		if ($this->iuserinfo !== null) {
			$iauthority .= $this->iuserinfo . '@';
		}
		if ($this->ihost !== null) {
			$iauthority .= $this->ihost;
		}
		if ($this->port !== null) {
			$iauthority .= ':' . $this->port;
		}
		return $iauthority;
	}

	/**
	 * Get the complete authority
	 *
	 * @return string
	 */
	protected function get_authority() {
		$iauthority = $this->get_iauthority();
		if (is_string($iauthority)) {
			return $this->to_uri($iauthority);
		}
		else {
			return $iauthority;
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  Exception/Http.php                                                                                  0000644                 00000003006 15227642210 0010132 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception based on HTTP response
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception;

use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\Http\StatusUnknown;

/**
 * Exception based on HTTP response
 *
 * @package Requests\Exceptions
 */
class Http extends Exception {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 0;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Unknown';

	/**
	 * Create a new exception
	 *
	 * There is no mechanism to pass in the status code, as this is set by the
	 * subclass used. Reason phrases can vary, however.
	 *
	 * @param string|null $reason Reason phrase
	 * @param mixed $data Associated data
	 */
	public function __construct($reason = null, $data = null) {
		if ($reason !== null) {
			$this->reason = $reason;
		}

		$message = sprintf('%d %s', $this->code, $this->reason);
		parent::__construct($message, 'httpresponse', $data, $this->code);
	}

	/**
	 * Get the status message.
	 *
	 * @return string
	 */
	public function getReason() {
		return $this->reason;
	}

	/**
	 * Get the correct exception class for a given error code
	 *
	 * @param int|bool $code HTTP status code, or false if unavailable
	 * @return string Exception class name to use
	 */
	public static function get_class($code) {
		if (!$code) {
			return StatusUnknown::class;
		}

		$class = sprintf('\WpOrg\Requests\Exception\Http\Status%d', $code);
		if (class_exists($class)) {
			return $class;
		}

		return StatusUnknown::class;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          Exception/Http/Status407.php                                                                        0000644                 00000000777 15227642210 0011664 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 407 Proxy Authentication Required responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 407 Proxy Authentication Required responses
 *
 * @package Requests\Exceptions
 */
final class Status407 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 407;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Proxy Authentication Required';
}
 Exception/Http/Status501.php                                                                        0000644                 00000000725 15227642210 0011650 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 501 Not Implemented responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 501 Not Implemented responses
 *
 * @package Requests\Exceptions
 */
final class Status501 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 501;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Not Implemented';
}
                                           Exception/Http/Status412.php                                                                        0000644                 00000000741 15227642210 0011647 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 412 Precondition Failed responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 412 Precondition Failed responses
 *
 * @package Requests\Exceptions
 */
final class Status412 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 412;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Precondition Failed';
}
                               Exception/Http/Status402.php                                                                        0000644                 00000000730 15227642210 0011644 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 402 Payment Required responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 402 Payment Required responses
 *
 * @package Requests\Exceptions
 */
final class Status402 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 402;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Payment Required';
}
                                        Exception/Http/Status505.php                                                                        0000644                 00000000766 15227642210 0011661 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 505 HTTP Version Not Supported responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 505 HTTP Version Not Supported responses
 *
 * @package Requests\Exceptions
 */
final class Status505 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 505;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'HTTP Version Not Supported';
}
          Exception/Http/Status405.php                                                                        0000644                 00000000736 15227642210 0011655 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 405 Method Not Allowed responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 405 Method Not Allowed responses
 *
 * @package Requests\Exceptions
 */
final class Status405 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 405;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Method Not Allowed';
}
                                  Exception/Http/Status404.php                                                                        0000644                 00000000703 15227642210 0011646 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 404 Not Found responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 404 Not Found responses
 *
 * @package Requests\Exceptions
 */
final class Status404 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 404;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Not Found';
}
                                                             Exception/Http/Status414.php                                                                        0000644                 00000000747 15227642210 0011657 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 414 Request-URI Too Large responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 414 Request-URI Too Large responses
 *
 * @package Requests\Exceptions
 */
final class Status414 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 414;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Request-URI Too Large';
}
                         Exception/Http/Status416.php                                                                        0000644                 00000001005 15227642210 0011645 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 416 Requested Range Not Satisfiable responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 416 Requested Range Not Satisfiable responses
 *
 * @package Requests\Exceptions
 */
final class Status416 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 416;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Requested Range Not Satisfiable';
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           Exception/Http/Status400.php                                                                        0000644                 00000000711 15227642210 0011641 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 400 Bad Request responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 400 Bad Request responses
 *
 * @package Requests\Exceptions
 */
final class Status400 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 400;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Bad Request';
}
                                                       Exception/Http/Status306.php                                                                        0000644                 00000000714 15227642210 0011651 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 306 Switch Proxy responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 306 Switch Proxy responses
 *
 * @package Requests\Exceptions
 */
final class Status306 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 306;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Switch Proxy';
}
                                                    Exception/Http/Status511.php                                                                        0000644                 00000001145 15227642210 0011646 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 511 Network Authentication Required responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 511 Network Authentication Required responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */
final class Status511 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 511;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Network Authentication Required';
}
                                                                                                                                                                                                                                                                                                                                                                                                                           Exception/Http/Status417.php                                                                        0000644                 00000000736 15227642210 0011660 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 417 Expectation Failed responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 417 Expectation Failed responses
 *
 * @package Requests\Exceptions
 */
final class Status417 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 417;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Expectation Failed';
}
                                  Exception/Http/StatusUnknown.php                                                                    0000644                 00000001712 15227642210 0012777 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for unknown status responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;
use WpOrg\Requests\Response;

/**
 * Exception for unknown status responses
 *
 * @package Requests\Exceptions
 */
final class StatusUnknown extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer|bool Code if available, false if an error occurred
	 */
	protected $code = 0;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Unknown';

	/**
	 * Create a new exception
	 *
	 * If `$data` is an instance of {@see \WpOrg\Requests\Response}, uses the status
	 * code from it. Otherwise, sets as 0
	 *
	 * @param string|null $reason Reason phrase
	 * @param mixed $data Associated data
	 */
	public function __construct($reason = null, $data = null) {
		if ($data instanceof Response) {
			$this->code = (int) $data->status_code;
		}

		parent::__construct($reason, $data);
	}
}
                                                      Exception/Http/Status503.php                                                                        0000644                 00000000741 15227642210 0011650 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 503 Service Unavailable responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 503 Service Unavailable responses
 *
 * @package Requests\Exceptions
 */
final class Status503 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 503;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Service Unavailable';
}
                               Exception/Http/Status410.php                                                                        0000644                 00000000664 15227642210 0011651 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 410 Gone responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 410 Gone responses
 *
 * @package Requests\Exceptions
 */
final class Status410 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 410;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Gone';
}
                                                                            Exception/Http/Status304.php                                                                        0000644                 00000000714 15227642210 0011647 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 304 Not Modified responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 304 Not Modified responses
 *
 * @package Requests\Exceptions
 */
final class Status304 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 304;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Not Modified';
}
                                                    Exception/Http/Status500.php                                                                        0000644                 00000000747 15227642210 0011653 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 500 Internal Server Error responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 500 Internal Server Error responses
 *
 * @package Requests\Exceptions
 */
final class Status500 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 500;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Internal Server Error';
}
                         Exception/Http/Status305.php                                                                        0000644                 00000000703 15227642210 0011646 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 305 Use Proxy responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 305 Use Proxy responses
 *
 * @package Requests\Exceptions
 */
final class Status305 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 305;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Use Proxy';
}
                                                             Exception/Http/error_log                                                                            0000644                 00000052342 15227642210 0011345 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [15-Jul-2026 23:52:12 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/StatusUnknown.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/StatusUnknown.php on line 18
[15-Jul-2026 23:52:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status404.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status404.php on line 17
[16-Jul-2026 00:56:22 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status501.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status501.php on line 17
[16-Jul-2026 00:56:30 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status417.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status417.php on line 17
[16-Jul-2026 02:08:15 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status500.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status500.php on line 17
[16-Jul-2026 02:08:23 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status411.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status411.php on line 17
[16-Jul-2026 04:11:20 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status428.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status428.php on line 21
[16-Jul-2026 04:11:33 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status405.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status405.php on line 17
[16-Jul-2026 04:12:08 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status429.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status429.php on line 21
[16-Jul-2026 04:12:21 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status414.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status414.php on line 17
[16-Jul-2026 04:12:34 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status406.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status406.php on line 17
[16-Jul-2026 08:54:08 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status505.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status505.php on line 17
[17-Jul-2026 06:51:53 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status305.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status305.php on line 17
[17-Jul-2026 06:51:57 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status304.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status304.php on line 17
[17-Jul-2026 06:52:02 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status400.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status400.php on line 17
[17-Jul-2026 06:52:07 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status418.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status418.php on line 21
[17-Jul-2026 06:52:12 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status410.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status410.php on line 17
[17-Jul-2026 06:52:17 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status402.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status402.php on line 17
[17-Jul-2026 15:31:00 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status415.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status415.php on line 17
[17-Jul-2026 17:19:01 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status500.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status500.php on line 17
[17-Jul-2026 17:19:01 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status411.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status411.php on line 17
[18-Jul-2026 06:30:37 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status431.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status431.php on line 21
[18-Jul-2026 06:30:41 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status502.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status502.php on line 17
[18-Jul-2026 06:30:46 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status416.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status416.php on line 17
[18-Jul-2026 06:30:51 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status401.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status401.php on line 17
[18-Jul-2026 06:30:56 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status504.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status504.php on line 17
[18-Jul-2026 06:31:01 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status407.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status407.php on line 17
[18-Jul-2026 06:31:06 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status412.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status412.php on line 17
[18-Jul-2026 06:31:11 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status306.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status306.php on line 17
[18-Jul-2026 06:31:16 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status503.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status503.php on line 17
[18-Jul-2026 06:31:21 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status409.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status409.php on line 17
[18-Jul-2026 06:31:31 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status403.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status403.php on line 17
[18-Jul-2026 14:15:38 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status408.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status408.php on line 17
[18-Jul-2026 14:15:38 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status413.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status413.php on line 17
[18-Jul-2026 14:15:38 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status511.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status511.php on line 21
[19-Jul-2026 09:03:27 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status431.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status431.php on line 21
[19-Jul-2026 09:03:27 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status502.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status502.php on line 17
[19-Jul-2026 09:03:27 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status416.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status416.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status407.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status407.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status401.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status401.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status504.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status504.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status306.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status306.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status503.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status503.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status412.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status412.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status403.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status403.php on line 17
[19-Jul-2026 09:03:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status409.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status409.php on line 17
[19-Jul-2026 09:18:53 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status505.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status505.php on line 17
[19-Jul-2026 11:58:24 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status417.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status417.php on line 17
[19-Jul-2026 11:58:29 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status501.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status501.php on line 17
[19-Jul-2026 19:58:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status408.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status408.php on line 17
[19-Jul-2026 19:58:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/StatusUnknown.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/StatusUnknown.php on line 18
[19-Jul-2026 19:58:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status404.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status404.php on line 17
[19-Jul-2026 19:58:29 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status413.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status413.php on line 17
[19-Jul-2026 19:58:29 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status511.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status511.php on line 21
[20-Jul-2026 07:51:30 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status415.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status415.php on line 17
[20-Jul-2026 07:51:30 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status305.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status305.php on line 17
[20-Jul-2026 07:51:35 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status304.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status304.php on line 17
[20-Jul-2026 07:51:40 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status400.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status400.php on line 17
[20-Jul-2026 07:51:45 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status402.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status402.php on line 17
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status428.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status428.php on line 21
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status405.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status405.php on line 17
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status429.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status429.php on line 21
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status414.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status414.php on line 17
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status406.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status406.php on line 17
[21-Jul-2026 01:43:54 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status418.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status418.php on line 21
[21-Jul-2026 01:43:55 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Http" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status410.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http/Status410.php on line 17
                                                                                                                                                                                                                                                                                              Exception/Http/Status406.php                                                                        0000644                 00000000722 15227642210 0011651 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 406 Not Acceptable responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 406 Not Acceptable responses
 *
 * @package Requests\Exceptions
 */
final class Status406 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 406;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Not Acceptable';
}
                                              Exception/Http/Status413.php                                                                        0000644                 00000000760 15227642210 0011651 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 413 Request Entity Too Large responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 413 Request Entity Too Large responses
 *
 * @package Requests\Exceptions
 */
final class Status413 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 413;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Request Entity Too Large';
}
                Exception/Http/Status409.php                                                                        0000644                 00000000700 15227642210 0011650 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 409 Conflict responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 409 Conflict responses
 *
 * @package Requests\Exceptions
 */
final class Status409 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 409;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Conflict';
}
                                                                Exception/Http/Status428.php                                                                        0000644                 00000001107 15227642210 0011653 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 428 Precondition Required responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 428 Precondition Required responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */
final class Status428 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 428;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Precondition Required';
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                         Exception/Http/Status403.php                                                                        0000644                 00000000703 15227642210 0011645 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 403 Forbidden responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 403 Forbidden responses
 *
 * @package Requests\Exceptions
 */
final class Status403 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 403;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Forbidden';
}
                                                             Exception/Http/Status429.php                                                                        0000644                 00000001163 15227642210 0011656 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 429 Too Many Requests responses
 *
 * @link https://tools.ietf.org/html/draft-nottingham-http-new-status-04
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 429 Too Many Requests responses
 *
 * @link https://tools.ietf.org/html/draft-nottingham-http-new-status-04
 *
 * @package Requests\Exceptions
 */
final class Status429 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 429;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Too Many Requests';
}
                                                                                                                                                                                                                                                                                                                                                                                                             Exception/Http/Status504.php                                                                        0000644                 00000000725 15227642210 0011653 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 504 Gateway Timeout responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 504 Gateway Timeout responses
 *
 * @package Requests\Exceptions
 */
final class Status504 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 504;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Gateway Timeout';
}
                                           Exception/Http/Status431.php                                                                        0000644                 00000001145 15227642210 0011647 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 431 Request Header Fields Too Large responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 431 Request Header Fields Too Large responses
 *
 * @link https://tools.ietf.org/html/rfc6585
 *
 * @package Requests\Exceptions
 */
final class Status431 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 431;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Request Header Fields Too Large';
}
                                                                                                                                                                                                                                                                                                                                                                                                                           Exception/Http/Status411.php                                                                        0000644                 00000000725 15227642210 0011650 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 411 Length Required responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 411 Length Required responses
 *
 * @package Requests\Exceptions
 */
final class Status411 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 411;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Length Required';
}
                                           Exception/Http/Status502.php                                                                        0000644                 00000000711 15227642210 0011644 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 502 Bad Gateway responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 502 Bad Gateway responses
 *
 * @package Requests\Exceptions
 */
final class Status502 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 502;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Bad Gateway';
}
                                                       Exception/Http/Status418.php                                                                        0000644                 00000001054 15227642210 0011653 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 418 I'm A Teapot responses
 *
 * @link https://tools.ietf.org/html/rfc2324
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 418 I'm A Teapot responses
 *
 * @link https://tools.ietf.org/html/rfc2324
 *
 * @package Requests\Exceptions
 */
final class Status418 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 418;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = "I'm A Teapot";
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    Exception/Http/Status401.php                                                                        0000644                 00000000714 15227642210 0011645 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 401 Unauthorized responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 401 Unauthorized responses
 *
 * @package Requests\Exceptions
 */
final class Status401 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 401;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Unauthorized';
}
                                                    Exception/Http/Status408.php                                                                        0000644                 00000000725 15227642210 0011656 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 408 Request Timeout responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 408 Request Timeout responses
 *
 * @package Requests\Exceptions
 */
final class Status408 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 408;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Request Timeout';
}
                                           Exception/Http/Status415.php                                                                        0000644                 00000000752 15227642210 0011654 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Exception for 415 Unsupported Media Type responses
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Http;

use WpOrg\Requests\Exception\Http;

/**
 * Exception for 415 Unsupported Media Type responses
 *
 * @package Requests\Exceptions
 */
final class Status415 extends Http {
	/**
	 * HTTP status code
	 *
	 * @var integer
	 */
	protected $code = 415;

	/**
	 * Reason phrase
	 *
	 * @var string
	 */
	protected $reason = 'Unsupported Media Type';
}
                      Exception/InvalidArgument.php                                                                       0000644                 00000002122 15227642210 0012302 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace WpOrg\Requests\Exception;

use InvalidArgumentException;

/**
 * Exception for an invalid argument passed.
 *
 * @package Requests\Exceptions
 * @since   2.0.0
 */
final class InvalidArgument extends InvalidArgumentException {

	/**
	 * Create a new invalid argument exception with a standardized text.
	 *
	 * @param int    $position The argument position in the function signature. 1-based.
	 * @param string $name     The argument name in the function signature.
	 * @param string $expected The argument type expected as a string.
	 * @param string $received The actual argument type received.
	 *
	 * @return \WpOrg\Requests\Exception\InvalidArgument
	 */
	public static function create($position, $name, $expected, $received) {
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
		$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

		return new self(
			sprintf(
				'%s::%s(): Argument #%d (%s) must be of type %s, %s given',
				$stack[1]['class'],
				$stack[1]['function'],
				$position,
				$name,
				$expected,
				$received
			)
		);
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                              Exception/Transport/Curl.php                                                                        0000644                 00000002565 15227642210 0012125 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * CURL Transport Exception.
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception\Transport;

use WpOrg\Requests\Exception\Transport;

/**
 * CURL Transport Exception.
 *
 * @package Requests\Exceptions
 */
final class Curl extends Transport {

	const EASY  = 'cURLEasy';
	const MULTI = 'cURLMulti';
	const SHARE = 'cURLShare';

	/**
	 * cURL error code
	 *
	 * @var integer
	 */
	protected $code = -1;

	/**
	 * Which type of cURL error
	 *
	 * EASY|MULTI|SHARE
	 *
	 * @var string
	 */
	protected $type = 'Unknown';

	/**
	 * Clear text error message
	 *
	 * @var string
	 */
	protected $reason = 'Unknown';

	/**
	 * Create a new exception.
	 *
	 * @param string $message Exception message.
	 * @param string $type    Exception type.
	 * @param mixed  $data    Associated data, if applicable.
	 * @param int    $code    Exception numerical code, if applicable.
	 */
	public function __construct($message, $type, $data = null, $code = 0) {
		if ($type !== null) {
			$this->type = $type;
		}

		if ($code !== null) {
			$this->code = (int) $code;
		}

		if ($message !== null) {
			$this->reason = $message;
		}

		$message = sprintf('%d %s', $this->code, $this->reason);
		parent::__construct($message, $this->type, $data, $this->code);
	}

	/**
	 * Get the error message.
	 *
	 * @return string
	 */
	public function getReason() {
		return $this->reason;
	}

}
                                                                                                                                           Exception/Transport/error_log                                                                       0000644                 00000001234 15227642210 0012414 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 21:07:28 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport/Curl.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17
[21-Jul-2026 00:38:45 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception\Transport" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport/Curl.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport/Curl.php on line 17
                                                                                                                                                                                                                                                                                                                                                                    Exception/error_log                                                                                 0000644                 00000003530 15227642210 0010421 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [15-Jul-2026 21:28:40 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport.php on line 17
[15-Jul-2026 21:28:44 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/ArgumentCount.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/ArgumentCount.php on line 20
[18-Jul-2026 14:15:45 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http.php on line 18
[20-Jul-2026 03:22:02 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Http.php on line 18
[20-Jul-2026 07:51:29 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/Transport.php on line 17
[20-Jul-2026 07:51:50 UTC] PHP Fatal error:  Uncaught Error: Class "WpOrg\Requests\Exception" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/ArgumentCount.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Exception/ArgumentCount.php on line 20
                                                                                                                                                                        Exception/ArgumentCount.php                                                                         0000644                 00000002664 15227642210 0012017 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

namespace WpOrg\Requests\Exception;

use WpOrg\Requests\Exception;

/**
 * Exception for when an incorrect number of arguments are passed to a method.
 *
 * Typically, this exception is used when all arguments for a method are optional,
 * but certain arguments need to be passed together, i.e. a method which can be called
 * with no arguments or with two arguments, but not with one argument.
 *
 * Along the same lines, this exception is also used if a method expects an array
 * with a certain number of elements and the provided number of elements does not comply.
 *
 * @package Requests\Exceptions
 * @since   2.0.0
 */
final class ArgumentCount extends Exception {

	/**
	 * Create a new argument count exception with a standardized text.
	 *
	 * @param string $expected The argument count expected as a phrase.
	 *                         For example: `at least 2 arguments` or `exactly 1 argument`.
	 * @param int    $received The actual argument count received.
	 * @param string $type     Exception type.
	 *
	 * @return \WpOrg\Requests\Exception\ArgumentCount
	 */
	public static function create($expected, $received, $type) {
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace
		$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);

		return new self(
			sprintf(
				'%s::%s() expects %s, %d given',
				$stack[1]['class'],
				$stack[1]['function'],
				$expected,
				$received
			),
			$type
		);
	}
}
                                                                            Exception/Transport.php                                                                             0000644                 00000000364 15227642210 0011213 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Transport Exception
 *
 * @package Requests\Exceptions
 */

namespace WpOrg\Requests\Exception;

use WpOrg\Requests\Exception;

/**
 * Transport Exception
 *
 * @package Requests\Exceptions
 */
class Transport extends Exception {}
                                                                                                                                                                                                                                                                            Response.php                                                                                        0000644                 00000010271 15227642210 0007055 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * HTTP response class
 *
 * Contains a response from \WpOrg\Requests\Requests::request()
 *
 * @package Requests
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Cookie\Jar;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\Http;
use WpOrg\Requests\Response\Headers;

/**
 * HTTP response class
 *
 * Contains a response from \WpOrg\Requests\Requests::request()
 *
 * @package Requests
 */
class Response {

	/**
	 * Response body
	 *
	 * @var string
	 */
	public $body = '';

	/**
	 * Raw HTTP data from the transport
	 *
	 * @var string
	 */
	public $raw = '';

	/**
	 * Headers, as an associative array
	 *
	 * @var \WpOrg\Requests\Response\Headers Array-like object representing headers
	 */
	public $headers = [];

	/**
	 * Status code, false if non-blocking
	 *
	 * @var integer|boolean
	 */
	public $status_code = false;

	/**
	 * Protocol version, false if non-blocking
	 *
	 * @var float|boolean
	 */
	public $protocol_version = false;

	/**
	 * Whether the request succeeded or not
	 *
	 * @var boolean
	 */
	public $success = false;

	/**
	 * Number of redirects the request used
	 *
	 * @var integer
	 */
	public $redirects = 0;

	/**
	 * URL requested
	 *
	 * @var string
	 */
	public $url = '';

	/**
	 * Previous requests (from redirects)
	 *
	 * @var array Array of \WpOrg\Requests\Response objects
	 */
	public $history = [];

	/**
	 * Cookies from the request
	 *
	 * @var \WpOrg\Requests\Cookie\Jar Array-like object representing a cookie jar
	 */
	public $cookies = [];

	/**
	 * Constructor
	 */
	public function __construct() {
		$this->headers = new Headers();
		$this->cookies = new Jar();
	}

	/**
	 * Is the response a redirect?
	 *
	 * @return boolean True if redirect (3xx status), false if not.
	 */
	public function is_redirect() {
		$code = $this->status_code;
		return in_array($code, [300, 301, 302, 303, 307], true) || $code > 307 && $code < 400;
	}

	/**
	 * Throws an exception if the request was not successful
	 *
	 * @param boolean $allow_redirects Set to false to throw on a 3xx as well
	 *
	 * @throws \WpOrg\Requests\Exception If `$allow_redirects` is false, and code is 3xx (`response.no_redirects`)
	 * @throws \WpOrg\Requests\Exception\Http On non-successful status code. Exception class corresponds to "Status" + code (e.g. {@see \WpOrg\Requests\Exception\Http\Status404})
	 */
	public function throw_for_status($allow_redirects = true) {
		if ($this->is_redirect()) {
			if ($allow_redirects !== true) {
				throw new Exception('Redirection not allowed', 'response.no_redirects', $this);
			}
		} elseif (!$this->success) {
			$exception = Http::get_class($this->status_code);
			throw new $exception(null, $this);
		}
	}

	/**
	 * JSON decode the response body.
	 *
	 * The method parameters are the same as those for the PHP native `json_decode()` function.
	 *
	 * @link https://php.net/json-decode
	 *
	 * @param bool|null $associative Optional. When `true`, JSON objects will be returned as associative arrays;
	 *                               When `false`, JSON objects will be returned as objects.
	 *                               When `null`, JSON objects will be returned as associative arrays
	 *                               or objects depending on whether `JSON_OBJECT_AS_ARRAY` is set in the flags.
	 *                               Defaults to `true` (in contrast to the PHP native default of `null`).
	 * @param int       $depth       Optional. Maximum nesting depth of the structure being decoded.
	 *                               Defaults to `512`.
	 * @param int       $options     Optional. Bitmask of JSON_BIGINT_AS_STRING, JSON_INVALID_UTF8_IGNORE,
	 *                               JSON_INVALID_UTF8_SUBSTITUTE, JSON_OBJECT_AS_ARRAY, JSON_THROW_ON_ERROR.
	 *                               Defaults to `0` (no options set).
	 *
	 * @return array
	 *
	 * @throws \WpOrg\Requests\Exception If `$this->body` is not valid json.
	 */
	public function decode_body($associative = true, $depth = 512, $options = 0) {
		$data = json_decode($this->body, $associative, $depth, $options);

		if (json_last_error() !== JSON_ERROR_NONE) {
			$last_error = json_last_error_msg();
			throw new Exception('Unable to parse JSON data: ' . $last_error, 'response.invalid', $this);
		}

		return $data;
	}
}
                                                                                                                                                                                                                                                                                                                                       Utility/CaseInsensitiveDictionary.php                                                               0000644                 00000005133 15227642210 0014045 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Case-insensitive dictionary, suitable for HTTP headers
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests\Utility;

use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use ReturnTypeWillChange;
use WpOrg\Requests\Exception;

/**
 * Case-insensitive dictionary, suitable for HTTP headers
 *
 * @package Requests\Utilities
 */
class CaseInsensitiveDictionary implements ArrayAccess, IteratorAggregate {
	/**
	 * Actual item data
	 *
	 * @var array
	 */
	protected $data = [];

	/**
	 * Creates a case insensitive dictionary.
	 *
	 * @param array $data Dictionary/map to convert to case-insensitive
	 */
	public function __construct(array $data = []) {
		foreach ($data as $offset => $value) {
			$this->offsetSet($offset, $value);
		}
	}

	/**
	 * Check if the given item exists
	 *
	 * @param string $offset Item key
	 * @return boolean Does the item exist?
	 */
	#[ReturnTypeWillChange]
	public function offsetExists($offset) {
		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if ($offset === null) {
			$offset = '';
		}

		return isset($this->data[$offset]);
	}

	/**
	 * Get the value for the item
	 *
	 * @param string $offset Item key
	 * @return string|null Item value (null if the item key doesn't exist)
	 */
	#[ReturnTypeWillChange]
	public function offsetGet($offset) {
		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if ($offset === null) {
			$offset = '';
		}

		if (!isset($this->data[$offset])) {
			return null;
		}

		return $this->data[$offset];
	}

	/**
	 * Set the given item
	 *
	 * @param string $offset Item name
	 * @param string $value Item value
	 *
	 * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`)
	 */
	#[ReturnTypeWillChange]
	public function offsetSet($offset, $value) {
		if ($offset === null) {
			throw new Exception('Object is a dictionary, not a list', 'invalidset');
		}

		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		$this->data[$offset] = $value;
	}

	/**
	 * Unset the given header
	 *
	 * @param string $offset The key for the item to unset.
	 */
	#[ReturnTypeWillChange]
	public function offsetUnset($offset) {
		if (is_string($offset)) {
			$offset = strtolower($offset);
		}

		if ($offset === null) {
			$offset = '';
		}

		unset($this->data[$offset]);
	}

	/**
	 * Get an iterator for the data
	 *
	 * @return \ArrayIterator
	 */
	#[ReturnTypeWillChange]
	public function getIterator() {
		return new ArrayIterator($this->data);
	}

	/**
	 * Get the headers as an array
	 *
	 * @return array Header data
	 */
	public function getAll() {
		return $this->data;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                                                     Utility/FilteredIterator.php                                                                        0000644                 00000004200 15227642210 0012165 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Iterator for arrays requiring filtered values
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests\Utility;

use ArrayIterator;
use ReturnTypeWillChange;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Iterator for arrays requiring filtered values
 *
 * @package Requests\Utilities
 */
final class FilteredIterator extends ArrayIterator {
	/**
	 * Callback to run as a filter
	 *
	 * @var callable
	 */
	private $callback;

	/**
	 * Create a new iterator
	 *
	 * @param array    $data     The array to be iterated on.
	 * @param callable $callback Callback to be called on each value
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not iterable.
	 */
	public function __construct($data, $callback) {
		if (is_object($data) === true || InputValidator::is_iterable($data) === false) {
			throw InvalidArgument::create(1, '$data', 'iterable', gettype($data));
		}

		parent::__construct($data);

		if (is_callable($callback)) {
			$this->callback = $callback;
		}
	}

	/**
	 * Prevent unserialization of the object for security reasons.
	 *
	 * @phpcs:disable PHPCompatibility.FunctionNameRestrictions.NewMagicMethods.__unserializeFound
	 *
	 * @param array $data Restored array of data originally serialized.
	 *
	 * @return void
	 */
	#[ReturnTypeWillChange]
	public function __unserialize($data) {}
	// phpcs:enable

	/**
	 * Perform reinitialization tasks.
	 *
	 * Prevents a callback from being injected during unserialization of an object.
	 *
	 * @return void
	 */
	public function __wakeup() {
		unset($this->callback);
	}

	/**
	 * Get the current item's value after filtering
	 *
	 * @return string
	 */
	#[ReturnTypeWillChange]
	public function current() {
		$value = parent::current();

		if (is_callable($this->callback)) {
			$value = call_user_func($this->callback, $value);
		}

		return $value;
	}

	/**
	 * Prevent creating a PHP value from a stored representation of the object for security reasons.
	 *
	 * @param string $data The serialized string.
	 *
	 * @return void
	 */
	#[ReturnTypeWillChange]
	public function unserialize($data) {}
}
                                                                                                                                                                                                                                                                                                                                                                                                Utility/InputValidator.php                                                                          0000644                 00000004720 15227642210 0011671 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Input validation utilities.
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests\Utility;

use ArrayAccess;
use CurlHandle;
use Traversable;

/**
 * Input validation utilities.
 *
 * @package Requests\Utilities
 */
final class InputValidator {

	/**
	 * Verify that a received input parameter is of type string or is "stringable".
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function is_string_or_stringable($input) {
		return is_string($input) || self::is_stringable_object($input);
	}

	/**
	 * Verify whether a received input parameter is usable as an integer array key.
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function is_numeric_array_key($input) {
		if (is_int($input)) {
			return true;
		}

		if (!is_string($input)) {
			return false;
		}

		return (bool) preg_match('`^-?[0-9]+$`', $input);
	}

	/**
	 * Verify whether a received input parameter is "stringable".
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function is_stringable_object($input) {
		return is_object($input) && method_exists($input, '__toString');
	}

	/**
	 * Verify whether a received input parameter is _accessible as if it were an array_.
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function has_array_access($input) {
		return is_array($input) || $input instanceof ArrayAccess;
	}

	/**
	 * Verify whether a received input parameter is "iterable".
	 *
	 * @internal The PHP native `is_iterable()` function was only introduced in PHP 7.1
	 * and this library still supports PHP 5.6.
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function is_iterable($input) {
		return is_array($input) || $input instanceof Traversable;
	}

	/**
	 * Verify whether a received input parameter is a Curl handle.
	 *
	 * The PHP Curl extension worked with resources prior to PHP 8.0 and with
	 * an instance of the `CurlHandle` class since PHP 8.0.
	 * {@link https://www.php.net/manual/en/migration80.incompatible.php#migration80.incompatible.resource2object}
	 *
	 * @param mixed $input Input parameter to verify.
	 *
	 * @return bool
	 */
	public static function is_curl_handle($input) {
		if (is_resource($input)) {
			return get_resource_type($input) === 'curl';
		}

		if (is_object($input)) {
			return $input instanceof CurlHandle;
		}

		return false;
	}
}
                                                Autoload.php                                                                                        0000644                 00000022167 15227642210 0007036 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Autoloader for Requests for PHP.
 *
 * Include this file if you'd like to avoid having to create your own autoloader.
 *
 * @package Requests
 * @since   2.0.0
 *
 * @codeCoverageIgnore
 */

namespace WpOrg\Requests;

/*
 * Ensure the autoloader is only declared once.
 * This safeguard is in place as this is the typical entry point for this library
 * and this file being required unconditionally could easily cause
 * fatal "Class already declared" errors.
 */
if (class_exists('WpOrg\Requests\Autoload') === false) {

	/**
	 * Autoloader for Requests for PHP.
	 *
	 * This autoloader supports the PSR-4 based Requests 2.0.0 classes in a case-sensitive manner
	 * as the most common server OS-es are case-sensitive and the file names are in mixed case.
	 *
	 * For the PSR-0 Requests 1.x BC-layer, requested classes will be treated case-insensitively.
	 *
	 * @package Requests
	 */
	final class Autoload {

		/**
		 * List of the old PSR-0 class names in lowercase as keys with their PSR-4 case-sensitive name as a value.
		 *
		 * @var array
		 */
		private static $deprecated_classes = [
			// Interfaces.
			'requests_auth'                              => '\WpOrg\Requests\Auth',
			'requests_hooker'                            => '\WpOrg\Requests\HookManager',
			'requests_proxy'                             => '\WpOrg\Requests\Proxy',
			'requests_transport'                         => '\WpOrg\Requests\Transport',

			// Classes.
			'requests_cookie'                            => '\WpOrg\Requests\Cookie',
			'requests_exception'                         => '\WpOrg\Requests\Exception',
			'requests_hooks'                             => '\WpOrg\Requests\Hooks',
			'requests_idnaencoder'                       => '\WpOrg\Requests\IdnaEncoder',
			'requests_ipv6'                              => '\WpOrg\Requests\Ipv6',
			'requests_iri'                               => '\WpOrg\Requests\Iri',
			'requests_response'                          => '\WpOrg\Requests\Response',
			'requests_session'                           => '\WpOrg\Requests\Session',
			'requests_ssl'                               => '\WpOrg\Requests\Ssl',
			'requests_auth_basic'                        => '\WpOrg\Requests\Auth\Basic',
			'requests_cookie_jar'                        => '\WpOrg\Requests\Cookie\Jar',
			'requests_proxy_http'                        => '\WpOrg\Requests\Proxy\Http',
			'requests_response_headers'                  => '\WpOrg\Requests\Response\Headers',
			'requests_transport_curl'                    => '\WpOrg\Requests\Transport\Curl',
			'requests_transport_fsockopen'               => '\WpOrg\Requests\Transport\Fsockopen',
			'requests_utility_caseinsensitivedictionary' => '\WpOrg\Requests\Utility\CaseInsensitiveDictionary',
			'requests_utility_filterediterator'          => '\WpOrg\Requests\Utility\FilteredIterator',
			'requests_exception_http'                    => '\WpOrg\Requests\Exception\Http',
			'requests_exception_transport'               => '\WpOrg\Requests\Exception\Transport',
			'requests_exception_transport_curl'          => '\WpOrg\Requests\Exception\Transport\Curl',
			'requests_exception_http_304'                => '\WpOrg\Requests\Exception\Http\Status304',
			'requests_exception_http_305'                => '\WpOrg\Requests\Exception\Http\Status305',
			'requests_exception_http_306'                => '\WpOrg\Requests\Exception\Http\Status306',
			'requests_exception_http_400'                => '\WpOrg\Requests\Exception\Http\Status400',
			'requests_exception_http_401'                => '\WpOrg\Requests\Exception\Http\Status401',
			'requests_exception_http_402'                => '\WpOrg\Requests\Exception\Http\Status402',
			'requests_exception_http_403'                => '\WpOrg\Requests\Exception\Http\Status403',
			'requests_exception_http_404'                => '\WpOrg\Requests\Exception\Http\Status404',
			'requests_exception_http_405'                => '\WpOrg\Requests\Exception\Http\Status405',
			'requests_exception_http_406'                => '\WpOrg\Requests\Exception\Http\Status406',
			'requests_exception_http_407'                => '\WpOrg\Requests\Exception\Http\Status407',
			'requests_exception_http_408'                => '\WpOrg\Requests\Exception\Http\Status408',
			'requests_exception_http_409'                => '\WpOrg\Requests\Exception\Http\Status409',
			'requests_exception_http_410'                => '\WpOrg\Requests\Exception\Http\Status410',
			'requests_exception_http_411'                => '\WpOrg\Requests\Exception\Http\Status411',
			'requests_exception_http_412'                => '\WpOrg\Requests\Exception\Http\Status412',
			'requests_exception_http_413'                => '\WpOrg\Requests\Exception\Http\Status413',
			'requests_exception_http_414'                => '\WpOrg\Requests\Exception\Http\Status414',
			'requests_exception_http_415'                => '\WpOrg\Requests\Exception\Http\Status415',
			'requests_exception_http_416'                => '\WpOrg\Requests\Exception\Http\Status416',
			'requests_exception_http_417'                => '\WpOrg\Requests\Exception\Http\Status417',
			'requests_exception_http_418'                => '\WpOrg\Requests\Exception\Http\Status418',
			'requests_exception_http_428'                => '\WpOrg\Requests\Exception\Http\Status428',
			'requests_exception_http_429'                => '\WpOrg\Requests\Exception\Http\Status429',
			'requests_exception_http_431'                => '\WpOrg\Requests\Exception\Http\Status431',
			'requests_exception_http_500'                => '\WpOrg\Requests\Exception\Http\Status500',
			'requests_exception_http_501'                => '\WpOrg\Requests\Exception\Http\Status501',
			'requests_exception_http_502'                => '\WpOrg\Requests\Exception\Http\Status502',
			'requests_exception_http_503'                => '\WpOrg\Requests\Exception\Http\Status503',
			'requests_exception_http_504'                => '\WpOrg\Requests\Exception\Http\Status504',
			'requests_exception_http_505'                => '\WpOrg\Requests\Exception\Http\Status505',
			'requests_exception_http_511'                => '\WpOrg\Requests\Exception\Http\Status511',
			'requests_exception_http_unknown'            => '\WpOrg\Requests\Exception\Http\StatusUnknown',
		];

		/**
		 * Register the autoloader.
		 *
		 * Note: the autoloader is *prepended* in the autoload queue.
		 * This is done to ensure that the Requests 2.0 autoloader takes precedence
		 * over a potentially (dependency-registered) Requests 1.x autoloader.
		 *
		 * @internal This method contains a safeguard against the autoloader being
		 * registered multiple times. This safeguard uses a global constant to
		 * (hopefully/in most cases) still function correctly, even if the
		 * class would be renamed.
		 *
		 * @return void
		 */
		public static function register() {
			if (defined('REQUESTS_AUTOLOAD_REGISTERED') === false) {
				spl_autoload_register([self::class, 'load'], true);
				define('REQUESTS_AUTOLOAD_REGISTERED', true);
			}
		}

		/**
		 * Autoloader.
		 *
		 * @param string $class_name Name of the class name to load.
		 *
		 * @return bool Whether a class was loaded or not.
		 */
		public static function load($class_name) {
			// Check that the class starts with "Requests" (PSR-0) or "WpOrg\Requests" (PSR-4).
			$psr_4_prefix_pos = strpos($class_name, 'WpOrg\\Requests\\');

			if (stripos($class_name, 'Requests') !== 0 && $psr_4_prefix_pos !== 0) {
				return false;
			}

			$class_lower = strtolower($class_name);

			if ($class_lower === 'requests') {
				// Reference to the original PSR-0 Requests class.
				$file = dirname(__DIR__) . '/library/Requests.php';
			} elseif ($psr_4_prefix_pos === 0) {
				// PSR-4 classname.
				$file = __DIR__ . '/' . strtr(substr($class_name, 15), '\\', '/') . '.php';
			}

			if (isset($file) && file_exists($file)) {
				include $file;
				return true;
			}

			/*
			 * Okay, so the class starts with "Requests", but we couldn't find the file.
			 * If this is one of the deprecated/renamed PSR-0 classes being requested,
			 * let's alias it to the new name and throw a deprecation notice.
			 */
			if (isset(self::$deprecated_classes[$class_lower])) {
				/*
				 * Integrators who cannot yet upgrade to the PSR-4 class names can silence deprecations
				 * by defining a `REQUESTS_SILENCE_PSR0_DEPRECATIONS` constant and setting it to `true`.
				 * The constant needs to be defined before the first deprecated class is requested
				 * via this autoloader.
				 */
				if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS') || REQUESTS_SILENCE_PSR0_DEPRECATIONS !== true) {
					// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
					trigger_error(
						'The PSR-0 `Requests_...` class names in the Requests library are deprecated.'
						. ' Switch to the PSR-4 `WpOrg\Requests\...` class names at your earliest convenience.',
						E_USER_DEPRECATED
					);

					// Prevent the deprecation notice from being thrown twice.
					if (!defined('REQUESTS_SILENCE_PSR0_DEPRECATIONS')) {
						define('REQUESTS_SILENCE_PSR0_DEPRECATIONS', true);
					}
				}

				// Create an alias and let the autoloader recursively kick in to load the PSR-4 class.
				return class_alias(self::$deprecated_classes[$class_lower], $class_name, true);
			}

			return false;
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                         Session.php                                                                                         0000644                 00000021623 15227642210 0006705 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Session handler for persistent requests and default parameters
 *
 * @package Requests\SessionHandler
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Cookie\Jar;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Requests;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Session handler for persistent requests and default parameters
 *
 * Allows various options to be set as default values, and merges both the
 * options and URL properties together. A base URL can be set for all requests,
 * with all subrequests resolved from this. Base options can be set (including
 * a shared cookie jar), then overridden for individual requests.
 *
 * @package Requests\SessionHandler
 */
class Session {
	/**
	 * Base URL for requests
	 *
	 * URLs will be made absolute using this as the base
	 *
	 * @var string|null
	 */
	public $url = null;

	/**
	 * Base headers for requests
	 *
	 * @var array
	 */
	public $headers = [];

	/**
	 * Base data for requests
	 *
	 * If both the base data and the per-request data are arrays, the data will
	 * be merged before sending the request.
	 *
	 * @var array
	 */
	public $data = [];

	/**
	 * Base options for requests
	 *
	 * The base options are merged with the per-request data for each request.
	 * The only default option is a shared cookie jar between requests.
	 *
	 * Values here can also be set directly via properties on the Session
	 * object, e.g. `$session->useragent = 'X';`
	 *
	 * @var array
	 */
	public $options = [];

	/**
	 * Create a new session
	 *
	 * @param string|Stringable|null $url Base URL for requests
	 * @param array $headers Default headers for requests
	 * @param array $data Default data for requests
	 * @param array $options Default options for requests
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or null.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not an array.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 */
	public function __construct($url = null, $headers = [], $data = [], $options = []) {
		if ($url !== null && InputValidator::is_string_or_stringable($url) === false) {
			throw InvalidArgument::create(1, '$url', 'string|Stringable|null', gettype($url));
		}

		if (is_array($headers) === false) {
			throw InvalidArgument::create(2, '$headers', 'array', gettype($headers));
		}

		if (is_array($data) === false) {
			throw InvalidArgument::create(3, '$data', 'array', gettype($data));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(4, '$options', 'array', gettype($options));
		}

		$this->url     = $url;
		$this->headers = $headers;
		$this->data    = $data;
		$this->options = $options;

		if (empty($this->options['cookies'])) {
			$this->options['cookies'] = new Jar();
		}
	}

	/**
	 * Get a property's value
	 *
	 * @param string $name Property name.
	 * @return mixed|null Property value, null if none found
	 */
	public function __get($name) {
		if (isset($this->options[$name])) {
			return $this->options[$name];
		}

		return null;
	}

	/**
	 * Set a property's value
	 *
	 * @param string $name Property name.
	 * @param mixed $value Property value
	 */
	public function __set($name, $value) {
		$this->options[$name] = $value;
	}

	/**
	 * Remove a property's value
	 *
	 * @param string $name Property name.
	 */
	public function __isset($name) {
		return isset($this->options[$name]);
	}

	/**
	 * Remove a property's value
	 *
	 * @param string $name Property name.
	 */
	public function __unset($name) {
		unset($this->options[$name]);
	}

	/**#@+
	 * @see \WpOrg\Requests\Session::request()
	 * @param string $url
	 * @param array $headers
	 * @param array $options
	 * @return \WpOrg\Requests\Response
	 */
	/**
	 * Send a GET request
	 */
	public function get($url, $headers = [], $options = []) {
		return $this->request($url, $headers, null, Requests::GET, $options);
	}

	/**
	 * Send a HEAD request
	 */
	public function head($url, $headers = [], $options = []) {
		return $this->request($url, $headers, null, Requests::HEAD, $options);
	}

	/**
	 * Send a DELETE request
	 */
	public function delete($url, $headers = [], $options = []) {
		return $this->request($url, $headers, null, Requests::DELETE, $options);
	}
	/**#@-*/

	/**#@+
	 * @see \WpOrg\Requests\Session::request()
	 * @param string $url
	 * @param array $headers
	 * @param array $data
	 * @param array $options
	 * @return \WpOrg\Requests\Response
	 */
	/**
	 * Send a POST request
	 */
	public function post($url, $headers = [], $data = [], $options = []) {
		return $this->request($url, $headers, $data, Requests::POST, $options);
	}

	/**
	 * Send a PUT request
	 */
	public function put($url, $headers = [], $data = [], $options = []) {
		return $this->request($url, $headers, $data, Requests::PUT, $options);
	}

	/**
	 * Send a PATCH request
	 *
	 * Note: Unlike {@see \WpOrg\Requests\Session::post()} and {@see \WpOrg\Requests\Session::put()},
	 * `$headers` is required, as the specification recommends that should send an ETag
	 *
	 * @link https://tools.ietf.org/html/rfc5789
	 */
	public function patch($url, $headers, $data = [], $options = []) {
		return $this->request($url, $headers, $data, Requests::PATCH, $options);
	}
	/**#@-*/

	/**
	 * Main interface for HTTP requests
	 *
	 * This method initiates a request and sends it via a transport before
	 * parsing.
	 *
	 * @see \WpOrg\Requests\Requests::request()
	 *
	 * @param string $url URL to request
	 * @param array $headers Extra headers to send with the request
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
	 * @param string $type HTTP request type (use \WpOrg\Requests\Requests constants)
	 * @param array $options Options for the request (see {@see \WpOrg\Requests\Requests::request()})
	 * @return \WpOrg\Requests\Response
	 *
	 * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`)
	 */
	public function request($url, $headers = [], $data = [], $type = Requests::GET, $options = []) {
		$request = $this->merge_request(compact('url', 'headers', 'data', 'options'));

		return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']);
	}

	/**
	 * Send multiple HTTP requests simultaneously
	 *
	 * @see \WpOrg\Requests\Requests::request_multiple()
	 *
	 * @param array $requests Requests data (see {@see \WpOrg\Requests\Requests::request_multiple()})
	 * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()})
	 * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object)
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 */
	public function request_multiple($requests, $options = []) {
		if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
			throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(2, '$options', 'array', gettype($options));
		}

		foreach ($requests as $key => $request) {
			$requests[$key] = $this->merge_request($request, false);
		}

		$options = array_merge($this->options, $options);

		// Disallow forcing the type, as that's a per request setting
		unset($options['type']);

		return Requests::request_multiple($requests, $options);
	}

	public function __wakeup() {
		throw new \LogicException( __CLASS__ . ' should never be unserialized' );
	}

	/**
	 * Merge a request's data with the default data
	 *
	 * @param array $request Request data (same form as {@see \WpOrg\Requests\Session::request_multiple()})
	 * @param boolean $merge_options Should we merge options as well?
	 * @return array Request data
	 */
	protected function merge_request($request, $merge_options = true) {
		if ($this->url !== null) {
			$request['url'] = Iri::absolutize($this->url, $request['url']);
			$request['url'] = $request['url']->uri;
		}

		if (empty($request['headers'])) {
			$request['headers'] = [];
		}

		$request['headers'] = array_merge($this->headers, $request['headers']);

		if (empty($request['data'])) {
			if (is_array($this->data)) {
				$request['data'] = $this->data;
			}
		} elseif (is_array($request['data']) && is_array($this->data)) {
			$request['data'] = array_merge($this->data, $request['data']);
		}

		if ($merge_options === true) {
			$request['options'] = array_merge($this->options, $request['options']);

			// Disallow forcing the type, as that's a per request setting
			unset($request['options']['type']);
		}

		return $request;
	}
}
                                                                                                             Transport.php                                                                                       0000644                 00000003010 15227642210 0007244 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Base HTTP transport
 *
 * @package Requests\Transport
 */

namespace WpOrg\Requests;

/**
 * Base HTTP transport
 *
 * @package Requests\Transport
 */
interface Transport {
	/**
	 * Perform a request
	 *
	 * @param string $url URL to request
	 * @param array $headers Associative array of request headers
	 * @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD
	 * @param array $options Request options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return string Raw HTTP result
	 */
	public function request($url, $headers = [], $data = [], $options = []);

	/**
	 * Send multiple requests simultaneously
	 *
	 * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see \WpOrg\Requests\Transport::request()}
	 * @param array $options Global options, see {@see \WpOrg\Requests\Requests::response()} for documentation
	 * @return array Array of \WpOrg\Requests\Response objects (may contain \WpOrg\Requests\Exception or string responses as well)
	 */
	public function request_multiple($requests, $options);

	/**
	 * Self-test whether the transport can be used.
	 *
	 * The available capabilities to test for can be found in {@see \WpOrg\Requests\Capability}.
	 *
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return bool Whether the transport can be used.
	 */
	public static function test($capabilities = []);
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        Auth/Basic.php                                                                                      0000644                 00000004755 15227642210 0007213 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Basic Authentication provider
 *
 * @package Requests\Authentication
 */

namespace WpOrg\Requests\Auth;

use WpOrg\Requests\Auth;
use WpOrg\Requests\Exception\ArgumentCount;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooks;

/**
 * Basic Authentication provider
 *
 * Provides a handler for Basic HTTP authentication via the Authorization
 * header.
 *
 * @package Requests\Authentication
 */
class Basic implements Auth {
	/**
	 * Username
	 *
	 * @var string
	 */
	public $user;

	/**
	 * Password
	 *
	 * @var string
	 */
	public $pass;

	/**
	 * Constructor
	 *
	 * @since 2.0 Throws an `InvalidArgument` exception.
	 * @since 2.0 Throws an `ArgumentCount` exception instead of the Requests base `Exception.
	 *
	 * @param array|null $args Array of user and password. Must have exactly two elements
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array or null.
	 * @throws \WpOrg\Requests\Exception\ArgumentCount   On incorrect number of array elements (`authbasicbadargs`).
	 */
	public function __construct($args = null) {
		if (is_array($args)) {
			if (count($args) !== 2) {
				throw ArgumentCount::create('an array with exactly two elements', count($args), 'authbasicbadargs');
			}

			list($this->user, $this->pass) = $args;
			return;
		}

		if ($args !== null) {
			throw InvalidArgument::create(1, '$args', 'array|null', gettype($args));
		}
	}

	/**
	 * Register the necessary callbacks
	 *
	 * @see \WpOrg\Requests\Auth\Basic::curl_before_send()
	 * @see \WpOrg\Requests\Auth\Basic::fsockopen_header()
	 * @param \WpOrg\Requests\Hooks $hooks Hook system
	 */
	public function register(Hooks $hooks) {
		$hooks->register('curl.before_send', [$this, 'curl_before_send']);
		$hooks->register('fsockopen.after_headers', [$this, 'fsockopen_header']);
	}

	/**
	 * Set cURL parameters before the data is sent
	 *
	 * @param resource|\CurlHandle $handle cURL handle
	 */
	public function curl_before_send(&$handle) {
		curl_setopt($handle, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
		curl_setopt($handle, CURLOPT_USERPWD, $this->getAuthString());
	}

	/**
	 * Add extra headers to the request before sending
	 *
	 * @param string $out HTTP header string
	 */
	public function fsockopen_header(&$out) {
		$out .= sprintf("Authorization: Basic %s\r\n", base64_encode($this->getAuthString()));
	}

	/**
	 * Get the authentication string (user:pass)
	 *
	 * @return string
	 */
	public function getAuthString() {
		return $this->user . ':' . $this->pass;
	}
}
                   Auth/error_log                                                                                      0000644                 00000001116 15227642210 0007362 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [18-Jul-2026 14:15:40 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Auth/Basic.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Auth/Basic.php on line 23
[20-Jul-2026 03:22:03 UTC] PHP Fatal error:  Uncaught Error: Interface "WpOrg\Requests\Auth" not found in /home/centerfor3d/public_html/wp-includes/Requests/src/Auth/Basic.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/Requests/src/Auth/Basic.php on line 23
                                                                                                                                                                                                                                                                                                                                                                                                                                                  Cookie/Jar.php                                                                                      0000644                 00000010413 15227642210 0007202 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Cookie holder object
 *
 * @package Requests\Cookies
 */

namespace WpOrg\Requests\Cookie;

use ArrayAccess;
use ArrayIterator;
use IteratorAggregate;
use ReturnTypeWillChange;
use WpOrg\Requests\Cookie;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\HookManager;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Response;

/**
 * Cookie holder object
 *
 * @package Requests\Cookies
 */
class Jar implements ArrayAccess, IteratorAggregate {
	/**
	 * Actual item data
	 *
	 * @var array
	 */
	protected $cookies = [];

	/**
	 * Create a new jar
	 *
	 * @param array $cookies Existing cookie values
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not an array.
	 */
	public function __construct($cookies = []) {
		if (is_array($cookies) === false) {
			throw InvalidArgument::create(1, '$cookies', 'array', gettype($cookies));
		}

		$this->cookies = $cookies;
	}

	/**
	 * Normalise cookie data into a \WpOrg\Requests\Cookie
	 *
	 * @param string|\WpOrg\Requests\Cookie $cookie Cookie header value, possibly pre-parsed (object).
	 * @param string                        $key    Optional. The name for this cookie.
	 * @return \WpOrg\Requests\Cookie
	 */
	public function normalize_cookie($cookie, $key = '') {
		if ($cookie instanceof Cookie) {
			return $cookie;
		}

		return Cookie::parse($cookie, $key);
	}

	/**
	 * Check if the given item exists
	 *
	 * @param string $offset Item key
	 * @return boolean Does the item exist?
	 */
	#[ReturnTypeWillChange]
	public function offsetExists($offset) {
		return isset($this->cookies[$offset]);
	}

	/**
	 * Get the value for the item
	 *
	 * @param string $offset Item key
	 * @return string|null Item value (null if offsetExists is false)
	 */
	#[ReturnTypeWillChange]
	public function offsetGet($offset) {
		if (!isset($this->cookies[$offset])) {
			return null;
		}

		return $this->cookies[$offset];
	}

	/**
	 * Set the given item
	 *
	 * @param string $offset Item name
	 * @param string $value Item value
	 *
	 * @throws \WpOrg\Requests\Exception On attempting to use dictionary as list (`invalidset`)
	 */
	#[ReturnTypeWillChange]
	public function offsetSet($offset, $value) {
		if ($offset === null) {
			throw new Exception('Object is a dictionary, not a list', 'invalidset');
		}

		$this->cookies[$offset] = $value;
	}

	/**
	 * Unset the given header
	 *
	 * @param string $offset The key for the item to unset.
	 */
	#[ReturnTypeWillChange]
	public function offsetUnset($offset) {
		unset($this->cookies[$offset]);
	}

	/**
	 * Get an iterator for the data
	 *
	 * @return \ArrayIterator
	 */
	#[ReturnTypeWillChange]
	public function getIterator() {
		return new ArrayIterator($this->cookies);
	}

	/**
	 * Register the cookie handler with the request's hooking system
	 *
	 * @param \WpOrg\Requests\HookManager $hooks Hooking system
	 */
	public function register(HookManager $hooks) {
		$hooks->register('requests.before_request', [$this, 'before_request']);
		$hooks->register('requests.before_redirect_check', [$this, 'before_redirect_check']);
	}

	/**
	 * Add Cookie header to a request if we have any
	 *
	 * As per RFC 6265, cookies are separated by '; '
	 *
	 * @param string $url
	 * @param array $headers
	 * @param array $data
	 * @param string $type
	 * @param array $options
	 */
	public function before_request($url, &$headers, &$data, &$type, &$options) {
		if (!$url instanceof Iri) {
			$url = new Iri($url);
		}

		if (!empty($this->cookies)) {
			$cookies = [];
			foreach ($this->cookies as $key => $cookie) {
				$cookie = $this->normalize_cookie($cookie, $key);

				// Skip expired cookies
				if ($cookie->is_expired()) {
					continue;
				}

				if ($cookie->domain_matches($url->host)) {
					$cookies[] = $cookie->format_for_header();
				}
			}

			$headers['Cookie'] = implode('; ', $cookies);
		}
	}

	/**
	 * Parse all cookies from a response and attach them to the response
	 *
	 * @param \WpOrg\Requests\Response $response Response as received.
	 */
	public function before_redirect_check(Response $response) {
		$url = $response->url;
		if (!$url instanceof Iri) {
			$url = new Iri($url);
		}

		$cookies           = Cookie::parse_from_headers($response->headers, $url);
		$this->cookies     = array_merge($this->cookies, $cookies);
		$response->cookies = $this;
	}
}
                                                                                                                                                                                                                                                     Requests.php                                                                                        0000644                 00000102321 15227642210 0007070 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Requests for PHP
 *
 * Inspired by Requests for Python.
 *
 * Based on concepts from SimplePie_File, RequestCore and WP_Http.
 *
 * @package Requests
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Auth\Basic;
use WpOrg\Requests\Capability;
use WpOrg\Requests\Cookie\Jar;
use WpOrg\Requests\Exception;
use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Hooks;
use WpOrg\Requests\IdnaEncoder;
use WpOrg\Requests\Iri;
use WpOrg\Requests\Proxy\Http;
use WpOrg\Requests\Response;
use WpOrg\Requests\Transport\Curl;
use WpOrg\Requests\Transport\Fsockopen;
use WpOrg\Requests\Utility\InputValidator;

/**
 * Requests for PHP
 *
 * Inspired by Requests for Python.
 *
 * Based on concepts from SimplePie_File, RequestCore and WP_Http.
 *
 * @package Requests
 */
class Requests {
	/**
	 * POST method
	 *
	 * @var string
	 */
	const POST = 'POST';

	/**
	 * PUT method
	 *
	 * @var string
	 */
	const PUT = 'PUT';

	/**
	 * GET method
	 *
	 * @var string
	 */
	const GET = 'GET';

	/**
	 * HEAD method
	 *
	 * @var string
	 */
	const HEAD = 'HEAD';

	/**
	 * DELETE method
	 *
	 * @var string
	 */
	const DELETE = 'DELETE';

	/**
	 * OPTIONS method
	 *
	 * @var string
	 */
	const OPTIONS = 'OPTIONS';

	/**
	 * TRACE method
	 *
	 * @var string
	 */
	const TRACE = 'TRACE';

	/**
	 * PATCH method
	 *
	 * @link https://tools.ietf.org/html/rfc5789
	 * @var string
	 */
	const PATCH = 'PATCH';

	/**
	 * Default size of buffer size to read streams
	 *
	 * @var integer
	 */
	const BUFFER_SIZE = 1160;

	/**
	 * Option defaults.
	 *
	 * @see \WpOrg\Requests\Requests::get_default_options()
	 * @see \WpOrg\Requests\Requests::request() for values returned by this method
	 *
	 * @since 2.0.0
	 *
	 * @var array
	 */
	const OPTION_DEFAULTS = [
		'timeout'          => 10,
		'connect_timeout'  => 10,
		'useragent'        => 'php-requests/' . self::VERSION,
		'protocol_version' => 1.1,
		'redirected'       => 0,
		'redirects'        => 10,
		'follow_redirects' => true,
		'blocking'         => true,
		'type'             => self::GET,
		'filename'         => false,
		'auth'             => false,
		'proxy'            => false,
		'cookies'          => false,
		'max_bytes'        => false,
		'idn'              => true,
		'hooks'            => null,
		'transport'        => null,
		'verify'           => null,
		'verifyname'       => true,
	];

	/**
	 * Default supported Transport classes.
	 *
	 * @since 2.0.0
	 *
	 * @var array
	 */
	const DEFAULT_TRANSPORTS = [
		Curl::class      => Curl::class,
		Fsockopen::class => Fsockopen::class,
	];

	/**
	 * Current version of Requests
	 *
	 * @var string
	 */
	const VERSION = '2.0.17';

	/**
	 * Selected transport name
	 *
	 * Use {@see \WpOrg\Requests\Requests::get_transport()} instead
	 *
	 * @var array
	 */
	public static $transport = [];

	/**
	 * Registered transport classes
	 *
	 * @var array
	 */
	protected static $transports = [];

	/**
	 * Default certificate path.
	 *
	 * @see \WpOrg\Requests\Requests::get_certificate_path()
	 * @see \WpOrg\Requests\Requests::set_certificate_path()
	 *
	 * @var string
	 */
	protected static $certificate_path = __DIR__ . '/../certificates/cacert.pem';

	/**
	 * All (known) valid deflate, gzip header magic markers.
	 *
	 * These markers relate to different compression levels.
	 *
	 * @link https://stackoverflow.com/a/43170354/482864 Marker source.
	 *
	 * @since 2.0.0
	 *
	 * @var array
	 */
	private static $magic_compression_headers = [
		"\x1f\x8b" => true, // Gzip marker.
		"\x78\x01" => true, // Zlib marker - level 1.
		"\x78\x5e" => true, // Zlib marker - level 2 to 5.
		"\x78\x9c" => true, // Zlib marker - level 6.
		"\x78\xda" => true, // Zlib marker - level 7 to 9.
	];

	/**
	 * This is a static class, do not instantiate it
	 *
	 * @codeCoverageIgnore
	 */
	private function __construct() {}

	/**
	 * Register a transport
	 *
	 * @param string $transport Transport class to add, must support the \WpOrg\Requests\Transport interface
	 */
	public static function add_transport($transport) {
		if (empty(self::$transports)) {
			self::$transports = self::DEFAULT_TRANSPORTS;
		}

		self::$transports[$transport] = $transport;
	}

	/**
	 * Get the fully qualified class name (FQCN) for a working transport.
	 *
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return string FQCN of the transport to use, or an empty string if no transport was
	 *                found which provided the requested capabilities.
	 */
	protected static function get_transport_class(array $capabilities = []) {
		// Caching code, don't bother testing coverage.
		// @codeCoverageIgnoreStart
		// Array of capabilities as a string to be used as an array key.
		ksort($capabilities);
		$cap_string = serialize($capabilities);

		// Don't search for a transport if it's already been done for these $capabilities.
		if (isset(self::$transport[$cap_string])) {
			return self::$transport[$cap_string];
		}

		// Ensure we will not run this same check again later on.
		self::$transport[$cap_string] = '';
		// @codeCoverageIgnoreEnd

		if (empty(self::$transports)) {
			self::$transports = self::DEFAULT_TRANSPORTS;
		}

		// Find us a working transport.
		foreach (self::$transports as $class) {
			if (!class_exists($class)) {
				continue;
			}

			$result = $class::test($capabilities);
			if ($result === true) {
				self::$transport[$cap_string] = $class;
				break;
			}
		}

		return self::$transport[$cap_string];
	}

	/**
	 * Get a working transport.
	 *
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return \WpOrg\Requests\Transport
	 * @throws \WpOrg\Requests\Exception If no valid transport is found (`notransport`).
	 */
	protected static function get_transport(array $capabilities = []) {
		$class = self::get_transport_class($capabilities);

		if ($class === '') {
			throw new Exception('No working transports found', 'notransport', self::$transports);
		}

		return new $class();
	}

	/**
	 * Checks to see if we have a transport for the capabilities requested.
	 *
	 * Supported capabilities can be found in the {@see \WpOrg\Requests\Capability}
	 * interface as constants.
	 *
	 * Example usage:
	 * `Requests::has_capabilities([Capability::SSL => true])`.
	 *
	 * @param array<string, bool> $capabilities Optional. Associative array of capabilities to test against, i.e. `['<capability>' => true]`.
	 * @return bool Whether the transport has the requested capabilities.
	 */
	public static function has_capabilities(array $capabilities = []) {
		return self::get_transport_class($capabilities) !== '';
	}

	/**#@+
	 * @see \WpOrg\Requests\Requests::request()
	 * @param string $url
	 * @param array $headers
	 * @param array $options
	 * @return \WpOrg\Requests\Response
	 */
	/**
	 * Send a GET request
	 */
	public static function get($url, $headers = [], $options = []) {
		return self::request($url, $headers, null, self::GET, $options);
	}

	/**
	 * Send a HEAD request
	 */
	public static function head($url, $headers = [], $options = []) {
		return self::request($url, $headers, null, self::HEAD, $options);
	}

	/**
	 * Send a DELETE request
	 */
	public static function delete($url, $headers = [], $options = []) {
		return self::request($url, $headers, null, self::DELETE, $options);
	}

	/**
	 * Send a TRACE request
	 */
	public static function trace($url, $headers = [], $options = []) {
		return self::request($url, $headers, null, self::TRACE, $options);
	}
	/**#@-*/

	/**#@+
	 * @see \WpOrg\Requests\Requests::request()
	 * @param string $url
	 * @param array $headers
	 * @param array $data
	 * @param array $options
	 * @return \WpOrg\Requests\Response
	 */
	/**
	 * Send a POST request
	 */
	public static function post($url, $headers = [], $data = [], $options = []) {
		return self::request($url, $headers, $data, self::POST, $options);
	}
	/**
	 * Send a PUT request
	 */
	public static function put($url, $headers = [], $data = [], $options = []) {
		return self::request($url, $headers, $data, self::PUT, $options);
	}

	/**
	 * Send an OPTIONS request
	 */
	public static function options($url, $headers = [], $data = [], $options = []) {
		return self::request($url, $headers, $data, self::OPTIONS, $options);
	}

	/**
	 * Send a PATCH request
	 *
	 * Note: Unlike {@see \WpOrg\Requests\Requests::post()} and {@see \WpOrg\Requests\Requests::put()},
	 * `$headers` is required, as the specification recommends that should send an ETag
	 *
	 * @link https://tools.ietf.org/html/rfc5789
	 */
	public static function patch($url, $headers, $data = [], $options = []) {
		return self::request($url, $headers, $data, self::PATCH, $options);
	}
	/**#@-*/

	/**
	 * Main interface for HTTP requests
	 *
	 * This method initiates a request and sends it via a transport before
	 * parsing.
	 *
	 * The `$options` parameter takes an associative array with the following
	 * options:
	 *
	 * - `timeout`: How long should we wait for a response?
	 *    Note: for cURL, a minimum of 1 second applies, as DNS resolution
	 *    operates at second-resolution only.
	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
	 * - `connect_timeout`: How long should we wait while trying to connect?
	 *    (float, seconds with a millisecond precision, default: 10, example: 0.01)
	 * - `useragent`: Useragent to send to the server
	 *    (string, default: php-requests/$version)
	 * - `follow_redirects`: Should we follow 3xx redirects?
	 *    (boolean, default: true)
	 * - `redirects`: How many times should we redirect before erroring?
	 *    (integer, default: 10)
	 * - `blocking`: Should we block processing on this request?
	 *    (boolean, default: true)
	 * - `filename`: File to stream the body to instead.
	 *    (string|boolean, default: false)
	 * - `auth`: Authentication handler or array of user/password details to use
	 *    for Basic authentication
	 *    (\WpOrg\Requests\Auth|array|boolean, default: false)
	 * - `proxy`: Proxy details to use for proxy by-passing and authentication
	 *    (\WpOrg\Requests\Proxy|array|string|boolean, default: false)
	 * - `max_bytes`: Limit for the response body size.
	 *    (integer|boolean, default: false)
	 * - `idn`: Enable IDN parsing
	 *    (boolean, default: true)
	 * - `transport`: Custom transport. Either a class name, or a
	 *    transport object. Defaults to the first working transport from
	 *    {@see \WpOrg\Requests\Requests::getTransport()}
	 *    (string|\WpOrg\Requests\Transport, default: {@see \WpOrg\Requests\Requests::getTransport()})
	 * - `hooks`: Hooks handler.
	 *    (\WpOrg\Requests\HookManager, default: new WpOrg\Requests\Hooks())
	 * - `verify`: Should we verify SSL certificates? Allows passing in a custom
	 *    certificate file as a string. (Using true uses the system-wide root
	 *    certificate store instead, but this may have different behaviour
	 *    across transports.)
	 *    (string|boolean, default: certificates/cacert.pem)
	 * - `verifyname`: Should we verify the common name in the SSL certificate?
	 *    (boolean, default: true)
	 * - `data_format`: How should we send the `$data` parameter?
	 *    (string, one of 'query' or 'body', default: 'query' for
	 *    HEAD/GET/DELETE, 'body' for POST/PUT/OPTIONS/PATCH)
	 *
	 * @param string|Stringable $url URL to request
	 * @param array $headers Extra headers to send with the request
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
	 * @param string $type HTTP request type (use Requests constants)
	 * @param array $options Options for the request (see description for more information)
	 * @return \WpOrg\Requests\Response
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string or Stringable.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $type argument is not a string.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`)
	 */
	public static function request($url, $headers = [], $data = [], $type = self::GET, $options = []) {
		if (InputValidator::is_string_or_stringable($url) === false) {
			throw InvalidArgument::create(1, '$url', 'string|Stringable', gettype($url));
		}

		if (is_string($type) === false) {
			throw InvalidArgument::create(4, '$type', 'string', gettype($type));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(5, '$options', 'array', gettype($options));
		}

		if (empty($options['type'])) {
			$options['type'] = $type;
		}

		$options = array_merge(self::get_default_options(), $options);

		self::set_defaults($url, $headers, $data, $type, $options);

		$options['hooks']->dispatch('requests.before_request', [&$url, &$headers, &$data, &$type, &$options]);

		if (!empty($options['transport'])) {
			$transport = $options['transport'];

			if (is_string($options['transport'])) {
				$transport = new $transport();
			}
		} else {
			$need_ssl     = (stripos($url, 'https://') === 0);
			$capabilities = [Capability::SSL => $need_ssl];
			$transport    = self::get_transport($capabilities);
		}

		$response = $transport->request($url, $headers, $data, $options);

		$options['hooks']->dispatch('requests.before_parse', [&$response, $url, $headers, $data, $type, $options]);

		return self::parse_response($response, $url, $headers, $data, $options);
	}

	/**
	 * Send multiple HTTP requests simultaneously
	 *
	 * The `$requests` parameter takes an associative or indexed array of
	 * request fields. The key of each request can be used to match up the
	 * request with the returned data, or with the request passed into your
	 * `multiple.request.complete` callback.
	 *
	 * The request fields value is an associative array with the following keys:
	 *
	 * - `url`: Request URL Same as the `$url` parameter to
	 *    {@see \WpOrg\Requests\Requests::request()}
	 *    (string, required)
	 * - `headers`: Associative array of header fields. Same as the `$headers`
	 *    parameter to {@see \WpOrg\Requests\Requests::request()}
	 *    (array, default: `array()`)
	 * - `data`: Associative array of data fields or a string. Same as the
	 *    `$data` parameter to {@see \WpOrg\Requests\Requests::request()}
	 *    (array|string, default: `array()`)
	 * - `type`: HTTP request type (use \WpOrg\Requests\Requests constants). Same as the `$type`
	 *    parameter to {@see \WpOrg\Requests\Requests::request()}
	 *    (string, default: `\WpOrg\Requests\Requests::GET`)
	 * - `cookies`: Associative array of cookie name to value, or cookie jar.
	 *    (array|\WpOrg\Requests\Cookie\Jar)
	 *
	 * If the `$options` parameter is specified, individual requests will
	 * inherit options from it. This can be used to use a single hooking system,
	 * or set all the types to `\WpOrg\Requests\Requests::POST`, for example.
	 *
	 * In addition, the `$options` parameter takes the following global options:
	 *
	 * - `complete`: A callback for when a request is complete. Takes two
	 *    parameters, a \WpOrg\Requests\Response/\WpOrg\Requests\Exception reference, and the
	 *    ID from the request array (Note: this can also be overridden on a
	 *    per-request basis, although that's a little silly)
	 *    (callback)
	 *
	 * @param array $requests Requests data (see description for more information)
	 * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()})
	 * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object)
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
	 */
	public static function request_multiple($requests, $options = []) {
		if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
			throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
		}

		if (is_array($options) === false) {
			throw InvalidArgument::create(2, '$options', 'array', gettype($options));
		}

		$options = array_merge(self::get_default_options(true), $options);

		if (!empty($options['hooks'])) {
			$options['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
			if (!empty($options['complete'])) {
				$options['hooks']->register('multiple.request.complete', $options['complete']);
			}
		}

		foreach ($requests as $id => &$request) {
			if (!isset($request['headers'])) {
				$request['headers'] = [];
			}

			if (!isset($request['data'])) {
				$request['data'] = [];
			}

			if (!isset($request['type'])) {
				$request['type'] = self::GET;
			}

			if (!isset($request['options'])) {
				$request['options']         = $options;
				$request['options']['type'] = $request['type'];
			} else {
				if (empty($request['options']['type'])) {
					$request['options']['type'] = $request['type'];
				}

				$request['options'] = array_merge($options, $request['options']);
			}

			self::set_defaults($request['url'], $request['headers'], $request['data'], $request['type'], $request['options']);

			// Ensure we only hook in once
			if ($request['options']['hooks'] !== $options['hooks']) {
				$request['options']['hooks']->register('transport.internal.parse_response', [static::class, 'parse_multiple']);
				if (!empty($request['options']['complete'])) {
					$request['options']['hooks']->register('multiple.request.complete', $request['options']['complete']);
				}
			}
		}

		unset($request);

		if (!empty($options['transport'])) {
			$transport = $options['transport'];

			if (is_string($options['transport'])) {
				$transport = new $transport();
			}
		} else {
			$transport = self::get_transport();
		}

		$responses = $transport->request_multiple($requests, $options);

		foreach ($responses as $id => &$response) {
			// If our hook got messed with somehow, ensure we end up with the
			// correct response
			if (is_string($response)) {
				$request = $requests[$id];
				self::parse_multiple($response, $request);
				$request['options']['hooks']->dispatch('multiple.request.complete', [&$response, $id]);
			}
		}

		return $responses;
	}

	/**
	 * Get the default options
	 *
	 * @see \WpOrg\Requests\Requests::request() for values returned by this method
	 * @param boolean $multirequest Is this a multirequest?
	 * @return array Default option values
	 */
	protected static function get_default_options($multirequest = false) {
		$defaults           = static::OPTION_DEFAULTS;
		$defaults['verify'] = self::$certificate_path;

		if ($multirequest !== false) {
			$defaults['complete'] = null;
		}

		return $defaults;
	}

	/**
	 * Get default certificate path.
	 *
	 * @return string Default certificate path.
	 */
	public static function get_certificate_path() {
		return self::$certificate_path;
	}

	/**
	 * Set default certificate path.
	 *
	 * @param string|Stringable|bool $path Certificate path, pointing to a PEM file.
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or boolean.
	 */
	public static function set_certificate_path($path) {
		if (InputValidator::is_string_or_stringable($path) === false && is_bool($path) === false) {
			throw InvalidArgument::create(1, '$path', 'string|Stringable|bool', gettype($path));
		}

		self::$certificate_path = $path;
	}

	/**
	 * Set the default values
	 *
	 * The $options parameter is updated with the results.
	 *
	 * @param string $url URL to request
	 * @param array $headers Extra headers to send with the request
	 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
	 * @param string $type HTTP request type
	 * @param array $options Options for the request
	 * @return void
	 *
	 * @throws \WpOrg\Requests\Exception When the $url is not an http(s) URL.
	 */
	protected static function set_defaults(&$url, &$headers, &$data, &$type, &$options) {
		if (!preg_match('/^http(s)?:\/\//i', $url, $matches)) {
			throw new Exception('Only HTTP(S) requests are handled.', 'nonhttp', $url);
		}

		if (empty($options['hooks'])) {
			$options['hooks'] = new Hooks();
		}

		if (is_array($options['auth'])) {
			$options['auth'] = new Basic($options['auth']);
		}

		if ($options['auth'] !== false) {
			$options['auth']->register($options['hooks']);
		}

		if (is_string($options['proxy']) || is_array($options['proxy'])) {
			$options['proxy'] = new Http($options['proxy']);
		}

		if ($options['proxy'] !== false) {
			$options['proxy']->register($options['hooks']);
		}

		if (is_array($options['cookies'])) {
			$options['cookies'] = new Jar($options['cookies']);
		} elseif (empty($options['cookies'])) {
			$options['cookies'] = new Jar();
		}

		if ($options['cookies'] !== false) {
			$options['cookies']->register($options['hooks']);
		}

		if ($options['idn'] !== false) {
			$iri       = new Iri($url);
			$iri->host = IdnaEncoder::encode($iri->ihost);
			$url       = $iri->uri;
		}

		// Massage the type to ensure we support it.
		$type = strtoupper($type);

		if (!isset($options['data_format'])) {
			if (in_array($type, [self::HEAD, self::GET, self::DELETE], true)) {
				$options['data_format'] = 'query';
			} else {
				$options['data_format'] = 'body';
			}
		}
	}

	/**
	 * HTTP response parser
	 *
	 * @param string $headers Full response text including headers and body
	 * @param string $url Original request URL
	 * @param array $req_headers Original $headers array passed to {@link request()}, in case we need to follow redirects
	 * @param array $req_data Original $data array passed to {@link request()}, in case we need to follow redirects
	 * @param array $options Original $options array passed to {@link request()}, in case we need to follow redirects
	 * @return \WpOrg\Requests\Response
	 *
	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`requests.no_crlf_separator`)
	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`noversion`)
	 * @throws \WpOrg\Requests\Exception On missing head/body separator (`toomanyredirects`)
	 */
	protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
		$return = new Response();
		if (!$options['blocking']) {
			return $return;
		}

		$return->raw  = $headers;
		$return->url  = (string) $url;
		$return->body = '';

		if (!$options['filename']) {
			$pos = strpos($headers, "\r\n\r\n");
			if ($pos === false) {
				// Crap!
				throw new Exception('Missing header/body separator', 'requests.no_crlf_separator');
			}

			$headers = substr($return->raw, 0, $pos);
			// Headers will always be separated from the body by two new lines - `\n\r\n\r`.
			$body = substr($return->raw, $pos + 4);
			if (!empty($body)) {
				$return->body = $body;
			}
		}

		// Pretend CRLF = LF for compatibility (RFC 2616, section 19.3)
		$headers = str_replace("\r\n", "\n", $headers);
		// Unfold headers (replace [CRLF] 1*( SP | HT ) with SP) as per RFC 2616 (section 2.2)
		$headers = preg_replace('/\n[ \t]/', ' ', $headers);
		$headers = explode("\n", $headers);
		preg_match('#^HTTP/(1\.\d)[ \t]+(\d+)#i', array_shift($headers), $matches);
		if (empty($matches)) {
			throw new Exception('Response could not be parsed', 'noversion', $headers);
		}

		$return->protocol_version = (float) $matches[1];
		$return->status_code      = (int) $matches[2];
		if ($return->status_code >= 200 && $return->status_code < 300) {
			$return->success = true;
		}

		foreach ($headers as $header) {
			list($key, $value) = explode(':', $header, 2);
			$value             = trim($value);
			preg_replace('#(\s+)#i', ' ', $value);
			$return->headers[$key] = $value;
		}

		if (isset($return->headers['transfer-encoding'])) {
			$return->body = self::decode_chunked($return->body);
			unset($return->headers['transfer-encoding']);
		}

		if (isset($return->headers['content-encoding'])) {
			$return->body = self::decompress($return->body);
		}

		//fsockopen and cURL compatibility
		if (isset($return->headers['connection'])) {
			unset($return->headers['connection']);
		}

		$options['hooks']->dispatch('requests.before_redirect_check', [&$return, $req_headers, $req_data, $options]);

		if ($return->is_redirect() && $options['follow_redirects'] === true) {
			if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
				if ($return->status_code === 303) {
					$options['type'] = self::GET;
				}

				$options['redirected']++;
				$location = $return->headers['location'];
				if (strpos($location, 'http://') !== 0 && strpos($location, 'https://') !== 0) {
					// relative redirect, for compatibility make it absolute
					$location = Iri::absolutize($url, $location);
					$location = $location->uri;
				}

				$hook_args = [
					&$location,
					&$req_headers,
					&$req_data,
					&$options,
					$return,
				];
				$options['hooks']->dispatch('requests.before_redirect', $hook_args);
				$redirected            = self::request($location, $req_headers, $req_data, $options['type'], $options);
				$redirected->history[] = $return;
				return $redirected;
			} elseif ($options['redirected'] >= $options['redirects']) {
				throw new Exception('Too many redirects', 'toomanyredirects', $return);
			}
		}

		$return->redirects = $options['redirected'];

		$options['hooks']->dispatch('requests.after_request', [&$return, $req_headers, $req_data, $options]);
		return $return;
	}

	/**
	 * Callback for `transport.internal.parse_response`
	 *
	 * Internal use only. Converts a raw HTTP response to a \WpOrg\Requests\Response
	 * while still executing a multiple request.
	 *
	 * `$response` is either set to a \WpOrg\Requests\Response instance, or a \WpOrg\Requests\Exception object
	 *
	 * @param string $response Full response text including headers and body (will be overwritten with Response instance)
	 * @param array $request Request data as passed into {@see \WpOrg\Requests\Requests::request_multiple()}
	 * @return void
	 */
	public static function parse_multiple(&$response, $request) {
		try {
			$url      = $request['url'];
			$headers  = $request['headers'];
			$data     = $request['data'];
			$options  = $request['options'];
			$response = self::parse_response($response, $url, $headers, $data, $options);
		} catch (Exception $e) {
			$response = $e;
		}
	}

	/**
	 * Decoded a chunked body as per RFC 2616
	 *
	 * @link https://tools.ietf.org/html/rfc2616#section-3.6.1
	 * @param string $data Chunked body
	 * @return string Decoded body
	 */
	protected static function decode_chunked($data) {
		if (!preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', trim($data))) {
			return $data;
		}

		$decoded = '';
		$encoded = $data;

		while (true) {
			$is_chunked = (bool) preg_match('/^([0-9a-f]+)(?:;(?:[\w-]*)(?:=(?:(?:[\w-]*)*|"(?:[^\r\n])*"))?)*\r\n/i', $encoded, $matches);
			if (!$is_chunked) {
				// Looks like it's not chunked after all
				return $data;
			}

			$length = hexdec(trim($matches[1]));
			if ($length === 0) {
				// Ignore trailer headers
				return $decoded;
			}

			$chunk_length = strlen($matches[0]);
			$decoded     .= substr($encoded, $chunk_length, $length);
			$encoded      = substr($encoded, $chunk_length + $length + 2);

			if (trim($encoded) === '0' || empty($encoded)) {
				return $decoded;
			}
		}

		// We'll never actually get down here
		// @codeCoverageIgnoreStart
	}
	// @codeCoverageIgnoreEnd

	/**
	 * Convert a key => value array to a 'key: value' array for headers
	 *
	 * @param iterable $dictionary Dictionary of header values
	 * @return array List of headers
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not iterable.
	 */
	public static function flatten($dictionary) {
		if (InputValidator::is_iterable($dictionary) === false) {
			throw InvalidArgument::create(1, '$dictionary', 'iterable', gettype($dictionary));
		}

		$return = [];
		foreach ($dictionary as $key => $value) {
			$return[] = sprintf('%s: %s', $key, $value);
		}

		return $return;
	}

	/**
	 * Decompress an encoded body
	 *
	 * Implements gzip, compress and deflate. Guesses which it is by attempting
	 * to decode.
	 *
	 * @param string $data Compressed data in one of the above formats
	 * @return string Decompressed string
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
	 */
	public static function decompress($data) {
		if (is_string($data) === false) {
			throw InvalidArgument::create(1, '$data', 'string', gettype($data));
		}

		if (trim($data) === '') {
			// Empty body does not need further processing.
			return $data;
		}

		$marker = substr($data, 0, 2);
		if (!isset(self::$magic_compression_headers[$marker])) {
			// Not actually compressed. Probably cURL ruining this for us.
			return $data;
		}

		if (function_exists('gzdecode')) {
			$decoded = @gzdecode($data);
			if ($decoded !== false) {
				return $decoded;
			}
		}

		if (function_exists('gzinflate')) {
			$decoded = @gzinflate($data);
			if ($decoded !== false) {
				return $decoded;
			}
		}

		$decoded = self::compatible_gzinflate($data);
		if ($decoded !== false) {
			return $decoded;
		}

		if (function_exists('gzuncompress')) {
			$decoded = @gzuncompress($data);
			if ($decoded !== false) {
				return $decoded;
			}
		}

		return $data;
	}

	/**
	 * Decompression of deflated string while staying compatible with the majority of servers.
	 *
	 * Certain Servers will return deflated data with headers which PHP's gzinflate()
	 * function cannot handle out of the box. The following function has been created from
	 * various snippets on the gzinflate() PHP documentation.
	 *
	 * Warning: Magic numbers within. Due to the potential different formats that the compressed
	 * data may be returned in, some "magic offsets" are needed to ensure proper decompression
	 * takes place. For a simple progmatic way to determine the magic offset in use, see:
	 * https://core.trac.wordpress.org/ticket/18273
	 *
	 * @since 1.6.0
	 * @link https://core.trac.wordpress.org/ticket/18273
	 * @link https://www.php.net/gzinflate#70875
	 * @link https://www.php.net/gzinflate#77336
	 *
	 * @param string $gz_data String to decompress.
	 * @return string|bool False on failure.
	 *
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string.
	 */
	public static function compatible_gzinflate($gz_data) {
		if (is_string($gz_data) === false) {
			throw InvalidArgument::create(1, '$gz_data', 'string', gettype($gz_data));
		}

		if (trim($gz_data) === '') {
			return false;
		}

		// Compressed data might contain a full zlib header, if so strip it for
		// gzinflate()
		if (substr($gz_data, 0, 3) === "\x1f\x8b\x08") {
			$i   = 10;
			$flg = ord(substr($gz_data, 3, 1));
			if ($flg > 0) {
				if ($flg & 4) {
					list($xlen) = unpack('v', substr($gz_data, $i, 2));
					$i         += 2 + $xlen;
				}

				if ($flg & 8) {
					$i = strpos($gz_data, "\0", $i) + 1;
				}

				if ($flg & 16) {
					$i = strpos($gz_data, "\0", $i) + 1;
				}

				if ($flg & 2) {
					$i += 2;
				}
			}

			$decompressed = self::compatible_gzinflate(substr($gz_data, $i));
			if ($decompressed !== false) {
				return $decompressed;
			}
		}

		// If the data is Huffman Encoded, we must first strip the leading 2
		// byte Huffman marker for gzinflate()
		// The response is Huffman coded by many compressors such as
		// java.util.zip.Deflater, Ruby's Zlib::Deflate, and .NET's
		// System.IO.Compression.DeflateStream.
		//
		// See https://decompres.blogspot.com/ for a quick explanation of this
		// data type
		$huffman_encoded = false;

		// low nibble of first byte should be 0x08
		list(, $first_nibble) = unpack('h', $gz_data);

		// First 2 bytes should be divisible by 0x1F
		list(, $first_two_bytes) = unpack('n', $gz_data);

		if ($first_nibble === 0x08 && ($first_two_bytes % 0x1F) === 0) {
			$huffman_encoded = true;
		}

		if ($huffman_encoded) {
			$decompressed = @gzinflate(substr($gz_data, 2));
			if ($decompressed !== false) {
				return $decompressed;
			}
		}

		if (substr($gz_data, 0, 4) === "\x50\x4b\x03\x04") {
			// ZIP file format header
			// Offset 6: 2 bytes, General-purpose field
			// Offset 26: 2 bytes, filename length
			// Offset 28: 2 bytes, optional field length
			// Offset 30: Filename field, followed by optional field, followed
			// immediately by data
			list(, $general_purpose_flag) = unpack('v', substr($gz_data, 6, 2));

			// If the file has been compressed on the fly, 0x08 bit is set of
			// the general purpose field. We can use this to differentiate
			// between a compressed document, and a ZIP file
			$zip_compressed_on_the_fly = ((0x08 & $general_purpose_flag) === 0x08);

			if (!$zip_compressed_on_the_fly) {
				// Don't attempt to decode a compressed zip file
				return $gz_data;
			}

			// Determine the first byte of data, based on the above ZIP header
			// offsets:
			$first_file_start = array_sum(unpack('v2', substr($gz_data, 26, 4)));
			$decompressed     = @gzinflate(substr($gz_data, 30 + $first_file_start));
			if ($decompressed !== false) {
				return $decompressed;
			}

			return false;
		}

		// Finally fall back to straight gzinflate
		$decompressed = @gzinflate($gz_data);
		if ($decompressed !== false) {
			return $decompressed;
		}

		// Fallback for all above failing, not expected, but included for
		// debugging and preventing regressions and to track stats
		$decompressed = @gzinflate(substr($gz_data, 2));
		if ($decompressed !== false) {
			return $decompressed;
		}

		return false;
	}
}
                                                                                                                                                                                                                                                                                                               Ssl.php                                                                                             0000644                 00000012461 15227642210 0006023 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * SSL utilities for Requests
 *
 * @package Requests\Utilities
 */

namespace WpOrg\Requests;

use WpOrg\Requests\Exception\InvalidArgument;
use WpOrg\Requests\Utility\InputValidator;

/**
 * SSL utilities for Requests
 *
 * Collection of utilities for working with and verifying SSL certificates.
 *
 * @package Requests\Utilities
 */
final class Ssl {
	/**
	 * Verify the certificate against common name and subject alternative names
	 *
	 * Unfortunately, PHP doesn't check the certificate against the alternative
	 * names, leading things like 'https://www.github.com/' to be invalid.
	 *
	 * @link https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
	 *
	 * @param string|Stringable $host Host name to verify against
	 * @param array $cert Certificate data from openssl_x509_parse()
	 * @return bool
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $host argument is not a string or a stringable object.
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $cert argument is not an array or array accessible.
	 */
	public static function verify_certificate($host, $cert) {
		if (InputValidator::is_string_or_stringable($host) === false) {
			throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host));
		}

		if (InputValidator::has_array_access($cert) === false) {
			throw InvalidArgument::create(2, '$cert', 'array|ArrayAccess', gettype($cert));
		}

		$has_dns_alt = false;

		// Check the subjectAltName
		if (!empty($cert['extensions']['subjectAltName'])) {
			$altnames = explode(',', $cert['extensions']['subjectAltName']);
			foreach ($altnames as $altname) {
				$altname = trim($altname);
				if (strpos($altname, 'DNS:') !== 0) {
					continue;
				}

				$has_dns_alt = true;

				// Strip the 'DNS:' prefix and trim whitespace
				$altname = trim(substr($altname, 4));

				// Check for a match
				if (self::match_domain($host, $altname) === true) {
					return true;
				}
			}

			if ($has_dns_alt === true) {
				return false;
			}
		}

		// Fall back to checking the common name if we didn't get any dNSName
		// alt names, as per RFC2818
		if (!empty($cert['subject']['CN'])) {
			// Check for a match
			return (self::match_domain($host, $cert['subject']['CN']) === true);
		}

		return false;
	}

	/**
	 * Verify that a reference name is valid
	 *
	 * Verifies a dNSName for HTTPS usage, (almost) as per Firefox's rules:
	 * - Wildcards can only occur in a name with more than 3 components
	 * - Wildcards can only occur as the last character in the first
	 *   component
	 * - Wildcards may be preceded by additional characters
	 *
	 * We modify these rules to be a bit stricter and only allow the wildcard
	 * character to be the full first component; that is, with the exclusion of
	 * the third rule.
	 *
	 * @param string|Stringable $reference Reference dNSName
	 * @return boolean Is the name valid?
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed argument is not a string or a stringable object.
	 */
	public static function verify_reference_name($reference) {
		if (InputValidator::is_string_or_stringable($reference) === false) {
			throw InvalidArgument::create(1, '$reference', 'string|Stringable', gettype($reference));
		}

		if ($reference === '') {
			return false;
		}

		if (preg_match('`\s`', $reference) > 0) {
			// Whitespace detected. This can never be a dNSName.
			return false;
		}

		$parts = explode('.', $reference);
		if ($parts !== array_filter($parts)) {
			// DNSName cannot contain two dots next to each other.
			return false;
		}

		// Check the first part of the name
		$first = array_shift($parts);

		if (strpos($first, '*') !== false) {
			// Check that the wildcard is the full part
			if ($first !== '*') {
				return false;
			}

			// Check that we have at least 3 components (including first)
			if (count($parts) < 2) {
				return false;
			}
		}

		// Check the remaining parts
		foreach ($parts as $part) {
			if (strpos($part, '*') !== false) {
				return false;
			}
		}

		// Nothing found, verified!
		return true;
	}

	/**
	 * Match a hostname against a dNSName reference
	 *
	 * @param string|Stringable $host Requested host
	 * @param string|Stringable $reference dNSName to match against
	 * @return boolean Does the domain match?
	 * @throws \WpOrg\Requests\Exception\InvalidArgument When either of the passed arguments is not a string or a stringable object.
	 */
	public static function match_domain($host, $reference) {
		if (InputValidator::is_string_or_stringable($host) === false) {
			throw InvalidArgument::create(1, '$host', 'string|Stringable', gettype($host));
		}

		// Check if the reference is blocklisted first
		if (self::verify_reference_name($reference) !== true) {
			return false;
		}

		// Check for a direct match
		if ((string) $host === (string) $reference) {
			return true;
		}

		// Calculate the valid wildcard match if the host is not an IP address
		// Also validates that the host has 3 parts or more, as per Firefox's ruleset,
		// as a wildcard reference is only allowed with 3 parts or more, so the
		// comparison will never match if host doesn't contain 3 parts or more as well.
		if (ip2long($host) === false) {
			$parts    = explode('.', $host);
			$parts[0] = '*';
			$wildcard = implode('.', $parts);
			if ($wildcard === (string) $reference) {
				return true;
			}
		}

		return false;
	}
}
                                                                                                                                                                                                               HookManager.php                                                                                     0000644                 00000001305 15227642210 0007450 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php
/**
 * Event dispatcher
 *
 * @package Requests\EventDispatcher
 */

namespace WpOrg\Requests;

/**
 * Event dispatcher
 *
 * @package Requests\EventDispatcher
 */
interface HookManager {
	/**
	 * Register a callback for a hook
	 *
	 * @param string $hook Hook name
	 * @param callable $callback Function/method to call on event
	 * @param int $priority Priority number. <0 is executed earlier, >0 is executed later
	 */
	public function register($hook, $callback, $priority = 0);

	/**
	 * Dispatch a message
	 *
	 * @param string $hook Hook name
	 * @param array $parameters Parameters to pass to callbacks
	 * @return boolean Successfulness
	 */
	public function dispatch($hook, $parameters = []);
}
                                                                                                                                                                                                                                                                                                                           Events/BeforeGenerateResultEvent.php                                                                0000644                 00000005251 15227644447 0013621 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Events;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
/**
 * Event dispatched before a prompt is sent to the AI model.
 *
 * This event allows listeners to inspect and modify the messages before they
 * are sent to the model. The event is not stoppable, meaning the model call
 * will always proceed regardless of listener actions.
 *
 * @since 0.4.0
 */
class BeforeGenerateResultEvent
{
    /**
     * @var list<Message> The messages to be sent to the model.
     */
    private array $messages;
    /**
     * @var ModelInterface The model that will process the prompt.
     */
    private ModelInterface $model;
    /**
     * @var CapabilityEnum|null The capability being used for generation.
     */
    private ?CapabilityEnum $capability;
    /**
     * Constructor.
     *
     * @since 0.4.0
     *
     * @param list<Message> $messages The messages to be sent to the model.
     * @param ModelInterface $model The model that will process the prompt.
     * @param CapabilityEnum|null $capability The capability being used for generation.
     */
    public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability)
    {
        $this->messages = $messages;
        $this->model = $model;
        $this->capability = $capability;
    }
    /**
     * Gets the messages to be sent to the model.
     *
     * @since 0.4.0
     *
     * @return list<Message> The messages.
     */
    public function getMessages(): array
    {
        return $this->messages;
    }
    /**
     * Gets the model that will process the prompt.
     *
     * @since 0.4.0
     *
     * @return ModelInterface The model.
     */
    public function getModel(): ModelInterface
    {
        return $this->model;
    }
    /**
     * Gets the capability being used for generation.
     *
     * @since 0.4.0
     *
     * @return CapabilityEnum|null The capability, or null if not specified.
     */
    public function getCapability(): ?CapabilityEnum
    {
        return $this->capability;
    }
    /**
     * Performs a deep clone of the event.
     *
     * This method ensures that message objects are cloned to prevent
     * modifications to the cloned event from affecting the original.
     * The model object is not cloned as it is a service object.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
    }
}
                                                                                                                                                                                                                                                                                                                                                       Events/AfterGenerateResultEvent.php                                                                 0000644                 00000006353 15227644447 0013464 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Events;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Event dispatched after a prompt has been sent to the AI model and a response received.
 *
 * This event allows listeners to inspect the result of the model call for logging,
 * analytics, or other post-processing purposes. The result object is immutable.
 *
 * @since 0.4.0
 */
class AfterGenerateResultEvent
{
    /**
     * @var list<Message> The messages that were sent to the model.
     */
    private array $messages;
    /**
     * @var ModelInterface The model that processed the prompt.
     */
    private ModelInterface $model;
    /**
     * @var CapabilityEnum|null The capability that was used for generation.
     */
    private ?CapabilityEnum $capability;
    /**
     * @var GenerativeAiResult The result from the model.
     */
    private GenerativeAiResult $result;
    /**
     * Constructor.
     *
     * @since 0.4.0
     *
     * @param list<Message> $messages The messages that were sent to the model.
     * @param ModelInterface $model The model that processed the prompt.
     * @param CapabilityEnum|null $capability The capability that was used for generation.
     * @param GenerativeAiResult $result The result from the model.
     */
    public function __construct(array $messages, ModelInterface $model, ?CapabilityEnum $capability, GenerativeAiResult $result)
    {
        $this->messages = $messages;
        $this->model = $model;
        $this->capability = $capability;
        $this->result = $result;
    }
    /**
     * Gets the messages that were sent to the model.
     *
     * @since 0.4.0
     *
     * @return list<Message> The messages.
     */
    public function getMessages(): array
    {
        return $this->messages;
    }
    /**
     * Gets the model that processed the prompt.
     *
     * @since 0.4.0
     *
     * @return ModelInterface The model.
     */
    public function getModel(): ModelInterface
    {
        return $this->model;
    }
    /**
     * Gets the capability that was used for generation.
     *
     * @since 0.4.0
     *
     * @return CapabilityEnum|null The capability, or null if not specified.
     */
    public function getCapability(): ?CapabilityEnum
    {
        return $this->capability;
    }
    /**
     * Gets the result from the model.
     *
     * @since 0.4.0
     *
     * @return GenerativeAiResult The result.
     */
    public function getResult(): GenerativeAiResult
    {
        return $this->result;
    }
    /**
     * Performs a deep clone of the event.
     *
     * This method ensures that message and result objects are cloned to prevent
     * modifications to the cloned event from affecting the original.
     * The model object is not cloned as it is a service object.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
        $this->result = clone $this->result;
    }
}
                                                                                                                                                                                                                                                                                     Tools/DTO/FunctionResponse.php                                                                      0000644                 00000007313 15227644447 0012332 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents a response to a function call.
 *
 * This DTO encapsulates the result of executing a function that was
 * requested by the AI model through a FunctionCall.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionResponseArrayShape array{id?: string, name?: string, response: mixed}
 *
 * @extends AbstractDataTransferObject<FunctionResponseArrayShape>
 */
class FunctionResponse extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_RESPONSE = 'response';
    /**
     * @var string|null The ID of the function call this is responding to.
     */
    private ?string $id;
    /**
     * @var string|null The name of the function that was called.
     */
    private ?string $name;
    /**
     * @var mixed The response data from the function.
     */
    private $response;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string|null $id The ID of the function call this is responding to.
     * @param string|null $name The name of the function that was called.
     * @param mixed $response The response data from the function.
     * @throws InvalidArgumentException If neither id nor name is provided.
     */
    public function __construct(?string $id, ?string $name, $response)
    {
        if ($id === null && $name === null) {
            throw new InvalidArgumentException('At least one of id or name must be provided.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->response = $response;
    }
    /**
     * Gets the function call ID.
     *
     * @since 0.1.0
     *
     * @return string|null The function call ID.
     */
    public function getId(): ?string
    {
        return $this->id;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string|null The function name.
     */
    public function getName(): ?string
    {
        return $this->name;
    }
    /**
     * Gets the function response.
     *
     * @since 0.1.0
     *
     * @return mixed The response data.
     */
    public function getResponse()
    {
        return $this->response;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The ID of the function call this is responding to.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function that was called.'], self::KEY_RESPONSE => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The response data from the function.']], 'anyOf' => [['required' => [self::KEY_RESPONSE, self::KEY_ID]], ['required' => [self::KEY_RESPONSE, self::KEY_NAME]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionResponseArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->id !== null) {
            $data[self::KEY_ID] = $this->id;
        }
        if ($this->name !== null) {
            $data[self::KEY_NAME] = $this->name;
        }
        $data[self::KEY_RESPONSE] = $this->response;
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_RESPONSE]);
        return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_RESPONSE]);
    }
}
                                                                                                                                                                                                                                                                                                                     Tools/DTO/FunctionDeclaration.php                                                                   0000644                 00000007005 15227644447 0012757 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents a function declaration for AI models.
 *
 * This DTO describes a function that can be called by the AI model,
 * including its name, description, and parameter schema.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionDeclarationArrayShape array{
 *     name: string,
 *     description: string,
 *     parameters?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<FunctionDeclarationArrayShape>
 */
class FunctionDeclaration extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_DESCRIPTION = 'description';
    public const KEY_PARAMETERS = 'parameters';
    /**
     * @var string The name of the function.
     */
    private string $name;
    /**
     * @var string A description of what the function does.
     */
    private string $description;
    /**
     * @var array<string, mixed>|null The JSON schema for the function parameters.
     */
    private ?array $parameters;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $name The name of the function.
     * @param string $description A description of what the function does.
     * @param array<string, mixed>|null $parameters The JSON schema for the function parameters.
     */
    public function __construct(string $name, string $description, ?array $parameters = null)
    {
        $this->name = $name;
        $this->description = $description;
        $this->parameters = $parameters;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string The function name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the function description.
     *
     * @since 0.1.0
     *
     * @return string The function description.
     */
    public function getDescription(): string
    {
        return $this->description;
    }
    /**
     * Gets the function parameters schema.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The parameters schema.
     */
    public function getParameters(): ?array
    {
        return $this->parameters;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'A description of what the function does.'], self::KEY_PARAMETERS => ['type' => 'object', 'description' => 'The JSON schema for the function parameters.', 'additionalProperties' => \true]], 'required' => [self::KEY_NAME, self::KEY_DESCRIPTION]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionDeclarationArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description];
        if ($this->parameters !== null) {
            $data[self::KEY_PARAMETERS] = $this->parameters;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_DESCRIPTION]);
        return new self($array[self::KEY_NAME], $array[self::KEY_DESCRIPTION], $array[self::KEY_PARAMETERS] ?? null);
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           Tools/DTO/WebSearch.php                                                                             0000644                 00000005502 15227644447 0010667 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents web search configuration for AI models.
 *
 * This DTO defines constraints for web searches that AI models can perform,
 * including allowed and disallowed domains.
 *
 * @since 0.1.0
 *
 * @phpstan-type WebSearchArrayShape array{allowedDomains?: string[], disallowedDomains?: string[]}
 *
 * @extends AbstractDataTransferObject<WebSearchArrayShape>
 */
class WebSearch extends AbstractDataTransferObject
{
    public const KEY_ALLOWED_DOMAINS = 'allowedDomains';
    public const KEY_DISALLOWED_DOMAINS = 'disallowedDomains';
    /**
     * @var string[] List of domains that are allowed for web search.
     */
    private array $allowedDomains;
    /**
     * @var string[] List of domains that are disallowed for web search.
     */
    private array $disallowedDomains;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string[] $allowedDomains List of domains that are allowed for web search.
     * @param string[] $disallowedDomains List of domains that are disallowed for web search.
     */
    public function __construct(array $allowedDomains = [], array $disallowedDomains = [])
    {
        $this->allowedDomains = $allowedDomains;
        $this->disallowedDomains = $disallowedDomains;
    }
    /**
     * Gets the allowed domains.
     *
     * @since 0.1.0
     *
     * @return string[] The allowed domains.
     */
    public function getAllowedDomains(): array
    {
        return $this->allowedDomains;
    }
    /**
     * Gets the disallowed domains.
     *
     * @since 0.1.0
     *
     * @return string[] The disallowed domains.
     */
    public function getDisallowedDomains(): array
    {
        return $this->disallowedDomains;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are allowed for web search.'], self::KEY_DISALLOWED_DOMAINS => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'List of domains that are disallowed for web search.']], 'required' => []];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return WebSearchArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ALLOWED_DOMAINS => $this->allowedDomains, self::KEY_DISALLOWED_DOMAINS => $this->disallowedDomains];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        return new self($array[self::KEY_ALLOWED_DOMAINS] ?? [], $array[self::KEY_DISALLOWED_DOMAINS] ?? []);
    }
}
                                                                                                                                                                                              Tools/DTO/error_log                                                                                 0000644                 00000004174 15227644447 0010234 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 05:39:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/WebSearch.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/WebSearch.php on line 19
[20-Jul-2026 05:40:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionResponse.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionResponse.php on line 20
[20-Jul-2026 05:40:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionDeclaration.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionDeclaration.php on line 23
[20-Jul-2026 05:40:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php on line 20
[21-Jul-2026 09:14:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php on line 20
[21-Jul-2026 09:33:33 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionResponse.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Tools/DTO/FunctionResponse.php on line 20
                                                                                                                                                                                                                                                                                                                                                                                                    Tools/DTO/FunctionCall.php                                                                          0000644                 00000007133 15227644447 0011407 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Tools\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents a function call request from an AI model.
 *
 * This DTO encapsulates information about a function that the AI model
 * wants to invoke, including the function name and its arguments.
 *
 * @since 0.1.0
 *
 * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed}
 *
 * @extends AbstractDataTransferObject<FunctionCallArrayShape>
 */
class FunctionCall extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_ARGS = 'args';
    /**
     * @var string|null Unique identifier for this function call.
     */
    private ?string $id;
    /**
     * @var string|null The name of the function to call.
     */
    private ?string $name;
    /**
     * @var mixed The arguments to pass to the function.
     */
    private $args;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string|null $id Unique identifier for this function call.
     * @param string|null $name The name of the function to call.
     * @param mixed $args The arguments to pass to the function.
     * @throws InvalidArgumentException If neither id nor name is provided.
     */
    public function __construct(?string $id = null, ?string $name = null, $args = null)
    {
        if ($id === null && $name === null) {
            throw new InvalidArgumentException('At least one of id or name must be provided.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->args = $args;
    }
    /**
     * Gets the function call ID.
     *
     * @since 0.1.0
     *
     * @return string|null The function call ID.
     */
    public function getId(): ?string
    {
        return $this->id;
    }
    /**
     * Gets the function name.
     *
     * @since 0.1.0
     *
     * @return string|null The function name.
     */
    public function getName(): ?string
    {
        return $this->name;
    }
    /**
     * Gets the function arguments.
     *
     * @since 0.1.0
     *
     * @return mixed The function arguments.
     */
    public function getArgs()
    {
        return $this->args;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this function call.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The name of the function to call.'], self::KEY_ARGS => ['type' => ['string', 'number', 'boolean', 'object', 'array', 'null'], 'description' => 'The arguments to pass to the function.']], 'anyOf' => [['required' => [self::KEY_ID]], ['required' => [self::KEY_NAME]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FunctionCallArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->id !== null) {
            $data[self::KEY_ID] = $this->id;
        }
        if ($this->name !== null) {
            $data[self::KEY_NAME] = $this->name;
        }
        if ($this->args !== null) {
            $data[self::KEY_ARGS] = $this->args;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        return new self($array[self::KEY_ID] ?? null, $array[self::KEY_NAME] ?? null, $array[self::KEY_ARGS] ?? null);
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                     Files/Enums/error_log                                                                               0000644                 00000001310 15227644447 0010624 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 16:14:58 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/Enums/MediaOrientationEnum.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/Enums/MediaOrientationEnum.php on line 19
[20-Jul-2026 16:14:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/Enums/FileTypeEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/Enums/FileTypeEnum.php on line 17
                                                                                                                                                                                                                                                                                                                        Files/Enums/FileTypeEnum.php                                                                        0000644                 00000001330 15227644447 0011770 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents the type of file storage.
 *
 * @method static self inline() Returns the inline file type.
 * @method static self remote() Returns the remote file type.
 * @method bool isInline() Checks if this is an inline file type.
 * @method bool isRemote() Checks if this is a remote file type.
 *
 * @since 0.1.0
 */
class FileTypeEnum extends AbstractEnum
{
    /**
     * Inline file with base64-encoded data.
     *
     * @var string
     */
    public const INLINE = 'inline';
    /**
     * Remote file referenced by URL.
     *
     * @var string
     */
    public const REMOTE = 'remote';
}
                                                                                                                                                                                                                                                                                                        Files/Enums/MediaOrientationEnum.php                                                                0000644                 00000001730 15227644447 0013506 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents the type of file storage.
 *
 * @method static self square() Returns the square orientation
 * @method static self landscape() Returns the landscape orientation.
 * @method static self portrait() Returns the portrait orientation.
 * @method bool isSquare() Checks if this is an square orientation
 * @method bool isLandscape() Checks if this is a landscape orientation.
 * @method bool isPortrait() Checks if this is a portrait orientation.
 *
 * @since 0.1.0
 */
class MediaOrientationEnum extends AbstractEnum
{
    /**
     * Square orientation.
     *
     * @var string
     */
    public const SQUARE = 'square';
    /**
     * Landscape orientation.
     *
     * @var string
     */
    public const LANDSCAPE = 'landscape';
    /**
     * Portrait orientation.
     *
     * @var string
     */
    public const PORTRAIT = 'portrait';
}
                                        Files/DTO/error_log                                                                                 0000644                 00000000526 15227644447 0010173 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 16:15:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/DTO/File.php:28
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Files/DTO/File.php on line 28
                                                                                                                                                                          Files/DTO/File.php                                                                                  0000644                 00000032326 15227644447 0007651 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\ValueObjects\MimeType;
/**
 * Represents a file in the AI client.
 *
 * This DTO automatically detects whether a file is a URL, base64 data, or local file path
 * and handles them appropriately.
 *
 * @since 0.1.0
 *
 * @phpstan-type FileArrayShape array{
 *     fileType: string,
 *     url?: string,
 *     mimeType: string,
 *     base64Data?: string
 * }
 *
 * @extends AbstractDataTransferObject<FileArrayShape>
 */
class File extends AbstractDataTransferObject
{
    public const KEY_FILE_TYPE = 'fileType';
    public const KEY_MIME_TYPE = 'mimeType';
    public const KEY_URL = 'url';
    public const KEY_BASE64_DATA = 'base64Data';
    /**
     * @var MimeType The MIME type of the file.
     */
    private MimeType $mimeType;
    /**
     * @var FileTypeEnum The type of file storage.
     */
    private FileTypeEnum $fileType;
    /**
     * @var string|null The URL for remote files.
     */
    private ?string $url = null;
    /**
     * @var string|null The base64 data for inline files.
     */
    private ?string $base64Data = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $file The file string (URL, base64 data, or local path).
     * @param string|null $mimeType The MIME type of the file (optional).
     * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
     */
    public function __construct(string $file, ?string $mimeType = null)
    {
        // Detect and process the file type (will set MIME type if possible)
        $this->detectAndProcessFile($file, $mimeType);
    }
    /**
     * Detects the file type and processes it accordingly.
     *
     * @since 0.1.0
     *
     * @param string $file The file string to process.
     * @param string|null $providedMimeType The explicitly provided MIME type.
     * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
     */
    private function detectAndProcessFile(string $file, ?string $providedMimeType): void
    {
        // Check if it's a URL
        if ($this->isUrl($file)) {
            $this->fileType = FileTypeEnum::remote();
            $this->url = $file;
            $this->mimeType = $this->determineMimeType($providedMimeType, null, $file);
            return;
        }
        // Data URI pattern.
        $dataUriPattern = '/^data:(?:([a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*' . '(?:;[a-zA-Z0-9\-]+=[a-zA-Z0-9\-]+)*)?;)?base64,([A-Za-z0-9+\/]*={0,2})$/';
        // Check if it's a data URI.
        if (preg_match($dataUriPattern, $file, $matches)) {
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $matches[2];
            // Extract just the base64 data
            $extractedMimeType = empty($matches[1]) ? null : $matches[1];
            $this->mimeType = $this->determineMimeType($providedMimeType, $extractedMimeType, null);
            return;
        }
        // Check if it's a local file path (before base64 check)
        if (file_exists($file) && is_file($file)) {
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $this->convertFileToBase64($file);
            $this->mimeType = $this->determineMimeType($providedMimeType, null, $file);
            return;
        }
        // Check if it's plain base64
        if (preg_match('/^[A-Za-z0-9+\/]*={0,2}$/', $file)) {
            if ($providedMimeType === null) {
                throw new InvalidArgumentException('MIME type is required when providing plain base64 data without data URI format.');
            }
            $this->fileType = FileTypeEnum::inline();
            $this->base64Data = $file;
            $this->mimeType = new MimeType($providedMimeType);
            return;
        }
        throw new InvalidArgumentException('Invalid file provided. Expected URL, base64 data, or valid local file path.');
    }
    /**
     * Checks if a string is a valid URL.
     *
     * @since 0.1.0
     *
     * @param string $string The string to check.
     * @return bool True if the string is a URL.
     */
    private function isUrl(string $string): bool
    {
        return filter_var($string, \FILTER_VALIDATE_URL) !== \false && preg_match('/^https?:\/\//i', $string);
    }
    /**
     * Converts a local file to base64.
     *
     * @since 0.1.0
     *
     * @param string $filePath The path to the local file.
     * @return string The base64-encoded file data.
     * @throws RuntimeException If the file cannot be read.
     */
    private function convertFileToBase64(string $filePath): string
    {
        $fileContent = @file_get_contents($filePath);
        if ($fileContent === \false) {
            throw new RuntimeException(sprintf('Unable to read file: %s', $filePath));
        }
        return base64_encode($fileContent);
    }
    /**
     * Gets the file type.
     *
     * @since 0.1.0
     *
     * @return FileTypeEnum The file type.
     */
    public function getFileType(): FileTypeEnum
    {
        return $this->fileType;
    }
    /**
     * Checks if the file is an inline file.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is inline (base64/data URI).
     */
    public function isInline(): bool
    {
        return $this->fileType->isInline();
    }
    /**
     * Checks if the file is a remote file.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is remote (URL).
     */
    public function isRemote(): bool
    {
        return $this->fileType->isRemote();
    }
    /**
     * Gets the URL for remote files.
     *
     * @since 0.1.0
     *
     * @return string|null The URL, or null if not a remote file.
     */
    public function getUrl(): ?string
    {
        return $this->url;
    }
    /**
     * Gets the base64-encoded data for inline files.
     *
     * @since 0.1.0
     *
     * @return string|null The plain base64-encoded data (without data URI prefix), or null if not an inline file.
     */
    public function getBase64Data(): ?string
    {
        return $this->base64Data;
    }
    /**
     * Gets the data as a data URI for inline files.
     *
     * @since 0.1.0
     *
     * @return string|null The data URI in format: data:[mimeType];base64,[data], or null if not an inline file.
     */
    public function getDataUri(): ?string
    {
        if ($this->base64Data === null) {
            return null;
        }
        return sprintf('data:%s;base64,%s', $this->getMimeType(), $this->base64Data);
    }
    /**
     * Gets the MIME type of the file as a string.
     *
     * @since 0.1.0
     *
     * @return string The MIME type string value.
     */
    public function getMimeType(): string
    {
        return (string) $this->mimeType;
    }
    /**
     * Gets the MIME type object.
     *
     * @since 0.1.0
     *
     * @return MimeType The MIME type object.
     */
    public function getMimeTypeObject(): MimeType
    {
        return $this->mimeType;
    }
    /**
     * Checks if the file is a video.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is a video.
     */
    public function isVideo(): bool
    {
        return $this->mimeType->isVideo();
    }
    /**
     * Checks if the file is an image.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is an image.
     */
    public function isImage(): bool
    {
        return $this->mimeType->isImage();
    }
    /**
     * Checks if the file is audio.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is audio.
     */
    public function isAudio(): bool
    {
        return $this->mimeType->isAudio();
    }
    /**
     * Checks if the file is text.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is text.
     */
    public function isText(): bool
    {
        return $this->mimeType->isText();
    }
    /**
     * Checks if the file is a document.
     *
     * @since 0.1.0
     *
     * @return bool True if the file is a document.
     */
    public function isDocument(): bool
    {
        return $this->mimeType->isDocument();
    }
    /**
     * Checks if the file is a specific MIME type.
     *
     * @since 0.1.0
     *
     * @param string $type The mime type to check (e.g. 'image', 'text', 'video', 'audio').
     *
     * @return bool True if the file is of the specified type.
     */
    public function isMimeType(string $type): bool
    {
        return $this->mimeType->isType($type);
    }
    /**
     * Determines the MIME type from various sources.
     *
     * @since 0.1.0
     *
     * @param string|null $providedMimeType The explicitly provided MIME type.
     * @param string|null $extractedMimeType The MIME type extracted from data URI.
     * @param string|null $pathOrUrl The file path or URL to extract extension from.
     * @return MimeType The determined MIME type.
     * @throws InvalidArgumentException If MIME type cannot be determined.
     */
    private function determineMimeType(?string $providedMimeType, ?string $extractedMimeType, ?string $pathOrUrl): MimeType
    {
        // Prefer explicitly provided MIME type
        if ($providedMimeType !== null) {
            return new MimeType($providedMimeType);
        }
        // Use extracted MIME type from data URI
        if ($extractedMimeType !== null) {
            return new MimeType($extractedMimeType);
        }
        // Try to determine from file extension
        if ($pathOrUrl !== null) {
            $parsedUrl = parse_url($pathOrUrl);
            $path = $parsedUrl['path'] ?? $pathOrUrl;
            // Remove query string and fragment if present
            $cleanPath = strtok($path, '?#');
            if ($cleanPath === \false) {
                $cleanPath = $path;
            }
            $extension = pathinfo($cleanPath, \PATHINFO_EXTENSION);
            if (!empty($extension)) {
                try {
                    return MimeType::fromExtension($extension);
                } catch (InvalidArgumentException $e) {
                    // Extension not recognized, continue to error
                    unset($e);
                }
            }
        }
        throw new InvalidArgumentException('Unable to determine MIME type. Please provide it explicitly.');
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'oneOf' => [['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::REMOTE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_URL => ['type' => 'string', 'format' => 'uri', 'description' => 'The URL to the remote file.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_URL]], ['properties' => [self::KEY_FILE_TYPE => ['type' => 'string', 'const' => FileTypeEnum::INLINE, 'description' => 'The file type.'], self::KEY_MIME_TYPE => ['type' => 'string', 'description' => 'The MIME type of the file.', 'pattern' => '^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9]' . '[a-zA-Z0-9!#$&\-\^_+.]*$'], self::KEY_BASE64_DATA => ['type' => 'string', 'description' => 'The base64-encoded file data.']], 'required' => [self::KEY_FILE_TYPE, self::KEY_MIME_TYPE, self::KEY_BASE64_DATA]]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return FileArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_FILE_TYPE => $this->fileType->value, self::KEY_MIME_TYPE => $this->getMimeType()];
        if ($this->url !== null) {
            $data[self::KEY_URL] = $this->url;
        } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) {
            $data[self::KEY_BASE64_DATA] = $this->base64Data;
        } else {
            throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.');
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_FILE_TYPE]);
        // Check which properties are set to determine how to construct the File
        $mimeType = $array[self::KEY_MIME_TYPE] ?? null;
        if (isset($array[self::KEY_URL])) {
            return new self($array[self::KEY_URL], $mimeType);
        } elseif (isset($array[self::KEY_BASE64_DATA])) {
            return new self($array[self::KEY_BASE64_DATA], $mimeType);
        } else {
            throw new InvalidArgumentException('File requires either url or base64Data.');
        }
    }
    /**
     * Performs a deep clone of the file.
     *
     * This method ensures that the MimeType value object is cloned to prevent
     * any shared references between the original and cloned file.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $this->mimeType = clone $this->mimeType;
    }
}
                                                                                                                                                                                                                                                                                                          Files/ValueObjects/MimeType.php                                                                     0000644                 00000017554 15227644447 0012471 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Files\ValueObjects;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Value object representing a MIME type.
 *
 * This immutable value object encapsulates MIME type validation and
 * provides convenient methods for checking MIME type categories.
 *
 * @since 0.1.0
 */
final class MimeType
{
    /**
     * @var string The MIME type value.
     */
    private string $value;
    /**
     * Common MIME type mappings for file extensions.
     *
     * @var array<string, string>
     */
    private static array $extensionMap = [
        // Text
        'txt' => 'text/plain',
        'html' => 'text/html',
        'htm' => 'text/html',
        'css' => 'text/css',
        'js' => 'application/javascript',
        'json' => 'application/json',
        'xml' => 'application/xml',
        'csv' => 'text/csv',
        'md' => 'text/markdown',
        // Images
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png' => 'image/png',
        'gif' => 'image/gif',
        'bmp' => 'image/bmp',
        'webp' => 'image/webp',
        'svg' => 'image/svg+xml',
        'ico' => 'image/x-icon',
        // Documents
        'pdf' => 'application/pdf',
        'doc' => 'application/msword',
        'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        'xls' => 'application/vnd.ms-excel',
        'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'ppt' => 'application/vnd.ms-powerpoint',
        'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
        'odt' => 'application/vnd.oasis.opendocument.text',
        'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
        // Archives
        'zip' => 'application/zip',
        'tar' => 'application/x-tar',
        'gz' => 'application/gzip',
        'rar' => 'application/x-rar-compressed',
        '7z' => 'application/x-7z-compressed',
        // Audio
        'mp3' => 'audio/mpeg',
        'wav' => 'audio/wav',
        'ogg' => 'audio/ogg',
        'flac' => 'audio/flac',
        'm4a' => 'audio/m4a',
        'aac' => 'audio/aac',
        // Video
        'mp4' => 'video/mp4',
        'avi' => 'video/x-msvideo',
        'mov' => 'video/quicktime',
        'wmv' => 'video/x-ms-wmv',
        'flv' => 'video/x-flv',
        'webm' => 'video/webm',
        'mkv' => 'video/x-matroska',
        // Fonts
        'ttf' => 'font/ttf',
        'otf' => 'font/otf',
        'woff' => 'font/woff',
        'woff2' => 'font/woff2',
        // Other
        'php' => 'application/x-httpd-php',
        'sh' => 'application/x-sh',
        'exe' => 'application/x-msdownload',
    ];
    /**
     * Document MIME types.
     *
     * @var array<string>
     */
    private static array $documentTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'application/vnd.oasis.opendocument.text', 'application/vnd.oasis.opendocument.spreadsheet'];
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $value The MIME type value.
     * @throws InvalidArgumentException If the MIME type is invalid.
     */
    public function __construct(string $value)
    {
        if (!self::isValid($value)) {
            throw new InvalidArgumentException(sprintf('Invalid MIME type: %s', $value));
        }
        $this->value = strtolower($value);
    }
    /**
     * Gets the primary known file extension for this MIME type.
     *
     * @since 0.1.0
     *
     * @return string The file extension (without the dot).
     * @throws InvalidArgumentException If no known extension exists for this MIME type.
     */
    public function toExtension(): string
    {
        // Reverse lookup for the MIME type to find the extension.
        $extension = array_search($this->value, self::$extensionMap, \true);
        if ($extension === \false) {
            throw new InvalidArgumentException(sprintf('No known extension for MIME type: %s', $this->value));
        }
        return $extension;
    }
    /**
     * Creates a MimeType from a file extension.
     *
     * @since 0.1.0
     *
     * @param string $extension The file extension (without the dot).
     * @return self The MimeType instance.
     * @throws InvalidArgumentException If the extension is not recognized.
     */
    public static function fromExtension(string $extension): self
    {
        $extension = strtolower($extension);
        if (!isset(self::$extensionMap[$extension])) {
            throw new InvalidArgumentException(sprintf('Unknown file extension: %s', $extension));
        }
        return new self(self::$extensionMap[$extension]);
    }
    /**
     * Checks if a MIME type string is valid.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type to validate.
     * @return bool True if valid.
     */
    public static function isValid(string $mimeType): bool
    {
        // Basic MIME type validation: type/subtype
        return (bool) preg_match('/^[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-\^_+.]*$/', $mimeType);
    }
    /**
     * Checks if this MIME type is a specific type.
     *
     * This method returns true when the stored MIME type begins with the
     * given prefix. For example, `"audio"` matches `"audio/mpeg"`.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type prefix to check (e.g., "audio", "image").
     * @return bool True if this MIME type is of the specified type.
     */
    public function isType(string $mimeType): bool
    {
        return str_starts_with($this->value, strtolower($mimeType) . '/');
    }
    /**
     * Checks if this is an image MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is an image type.
     */
    public function isImage(): bool
    {
        return $this->isType('image');
    }
    /**
     * Checks if this is an audio MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is an audio type.
     */
    public function isAudio(): bool
    {
        return $this->isType('audio');
    }
    /**
     * Checks if this is a video MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a video type.
     */
    public function isVideo(): bool
    {
        return $this->isType('video');
    }
    /**
     * Checks if this is a text MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a text type.
     */
    public function isText(): bool
    {
        return $this->isType('text');
    }
    /**
     * Checks if this is a document MIME type.
     *
     * @since 0.1.0
     *
     * @return bool True if this is a document type.
     */
    public function isDocument(): bool
    {
        return in_array($this->value, self::$documentTypes, \true);
    }
    /**
     * Checks if this MIME type equals another.
     *
     * @since 0.1.0
     *
     * @param self|string $other The other MIME type to compare.
     * @return bool True if equal.
     * @throws InvalidArgumentException If the other MIME type is invalid.
     */
    public function equals($other): bool
    {
        if ($other instanceof self) {
            return $this->value === $other->value;
        }
        if (is_string($other)) {
            return $this->value === strtolower($other);
        }
        throw new InvalidArgumentException(sprintf('Invalid MIME type comparison: %s', gettype($other)));
    }
    /**
     * Gets the string representation of the MIME type.
     *
     * @since 0.1.0
     *
     * @return string The MIME type value.
     */
    public function __toString(): string
    {
        return $this->value;
    }
}
                                                                                                                                                    Common/AbstractDataTransferObject.php                                                               0000644                 00000011175 15227644447 0013722 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common;

use JsonSerializable;
use stdClass;
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Abstract base class for all Data Value Objects in the AI Client.
 *
 * This abstract class consolidates the common functionality needed by all
 * data transfer objects:
 * - Array transformation for data manipulation
 * - JSON schema support for validation and documentation
 * - JSON serialization with proper empty object handling
 *
 * All DTOs in the AI Client should extend this class to ensure
 * consistent behavior across the codebase.
 *
 * @since 0.1.0
 *
 * @template TArrayShape of array<string, mixed>
 * @implements WithArrayTransformationInterface<TArrayShape>
 */
abstract class AbstractDataTransferObject implements WithArrayTransformationInterface, WithJsonSchemaInterface, JsonSerializable
{
    /**
     * Validates that required keys exist in the array data.
     *
     * @since 0.1.0
     *
     * @param array<mixed> $data The array data to validate.
     * @param string[] $requiredKeys The keys that must be present.
     * @throws InvalidArgumentException If any required key is missing.
     */
    protected static function validateFromArrayData(array $data, array $requiredKeys): void
    {
        $missingKeys = [];
        foreach ($requiredKeys as $key) {
            if (!array_key_exists($key, $data)) {
                $missingKeys[] = $key;
            }
        }
        if (!empty($missingKeys)) {
            throw new InvalidArgumentException(sprintf('%s::fromArray() missing required keys: %s', static::class, implode(', ', $missingKeys)));
        }
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function isArrayShape(array $array): bool
    {
        try {
            /** @var TArrayShape $array */
            static::fromArray($array);
            return \true;
        } catch (InvalidArgumentException $e) {
            return \false;
        }
    }
    /**
     * Converts the object to a JSON-serializable format.
     *
     * This method uses the toArray() method and then processes the result
     * based on the JSON schema to ensure proper object representation for
     * empty arrays.
     *
     * @since 0.1.0
     *
     * @return mixed The JSON-serializable representation.
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        $data = $this->toArray();
        $schema = static::getJsonSchema();
        return $this->convertEmptyArraysToObjects($data, $schema);
    }
    /**
     * Recursively converts empty arrays to stdClass objects where the schema expects objects.
     *
     * @since 0.1.0
     *
     * @param mixed $data The data to process.
     * @param array<mixed, mixed> $schema The JSON schema for the data.
     * @return mixed The processed data.
     */
    private function convertEmptyArraysToObjects($data, array $schema)
    {
        // If data is an empty array and schema expects object, convert to stdClass
        if (is_array($data) && empty($data) && isset($schema['type']) && $schema['type'] === 'object') {
            return new stdClass();
        }
        // If data is an array with content, recursively process nested structures
        if (is_array($data)) {
            // Handle object properties
            if (isset($schema['properties']) && is_array($schema['properties'])) {
                foreach ($data as $key => $value) {
                    if (isset($schema['properties'][$key]) && is_array($schema['properties'][$key])) {
                        $data[$key] = $this->convertEmptyArraysToObjects($value, $schema['properties'][$key]);
                    }
                }
            }
            // Handle array items
            if (isset($schema['items']) && is_array($schema['items'])) {
                foreach ($data as $index => $item) {
                    $data[$index] = $this->convertEmptyArraysToObjects($item, $schema['items']);
                }
            }
            // Handle oneOf/anyOf schemas - just use the first one
            foreach (['oneOf', 'anyOf'] as $keyword) {
                if (isset($schema[$keyword]) && is_array($schema[$keyword])) {
                    foreach ($schema[$keyword] as $possibleSchema) {
                        if (is_array($possibleSchema)) {
                            return $this->convertEmptyArraysToObjects($data, $possibleSchema);
                        }
                    }
                }
            }
        }
        return $data;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                   Common/Contracts/AiClientExceptionInterface.php                                                     0000644                 00000000527 15227644447 0015660 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

use Throwable;
/**
 * Base interface for all AI Client exceptions.
 *
 * This interface allows callers to catch all AI Client specific exceptions
 * with a single catch statement.
 *
 * @since 0.2.0
 */
interface AiClientExceptionInterface extends Throwable
{
}
                                                                                                                                                                         Common/Contracts/WithArrayTransformationInterface.php                                               0000644                 00000002033 15227644447 0017144 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that support array transformation.
 *
 * @since 0.1.0
 *
 * @template TArrayShape of array<string, mixed>
 */
interface WithArrayTransformationInterface
{
    /**
     * Converts the object to an array representation.
     *
     * @since 0.1.0
     *
     * @return TArrayShape The array representation.
     */
    public function toArray(): array;
    /**
     * Creates an instance from array data.
     *
     * @since 0.1.0
     *
     * @param TArrayShape $array The array data.
     * @return self<TArrayShape> The created instance.
     */
    public static function fromArray(array $array): self;
    /**
     * Checks if the array is a valid shape for this object.
     *
     * @since 0.1.0
     *
     * @param array<mixed> $array The array to check.
     * @return bool True if the array is a valid shape.
     * @phpstan-assert-if-true TArrayShape $array
     */
    public static function isArrayShape(array $array): bool;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     Common/Contracts/CachesDataInterface.php                                                            0000644                 00000000542 15227644447 0014266 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that cache data.
 *
 * @since 0.4.0
 */
interface CachesDataInterface
{
    /**
     * Invalidates all caches managed by this object.
     *
     * @since 0.4.0
     *
     * @return void
     */
    public function invalidateCaches(): void;
}
                                                                                                                                                              Common/Contracts/WithJsonSchemaInterface.php                                                        0000644                 00000001136 15227644447 0015174 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Contracts;

/**
 * Interface for objects that can provide their JSON schema representation.
 *
 * This interface is implemented by DTOs to provide a consistent way to retrieve
 * their JSON schema for validation and serialization purposes.
 *
 * @since 0.1.0
 */
interface WithJsonSchemaInterface
{
    /**
     * Gets the JSON schema representation of the object.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> The JSON schema as an associative array.
     */
    public static function getJsonSchema(): array;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                  Common/error_log                                                                                    0000644                 00000001440 15227644447 0007727 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 21:07:28 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/AbstractDataTransferObject.php:28
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/AbstractDataTransferObject.php on line 28
[21-Jul-2026 00:38:45 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/AbstractDataTransferObject.php:28
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/AbstractDataTransferObject.php on line 28
                                                                                                                                                                                                                                Common/Traits/WithDataCachingTrait.php                                                              0000644                 00000011762 15227644447 0013767 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Traits;

use WordPress\AiClient\AiClient;
/**
 * Trait for objects that cache data using PSR-16 cache with in-memory fallback.
 *
 * When a PSR-16 cache is configured via AiClient::setCache(), data is stored persistently.
 * Otherwise, data is cached in-memory for the duration of the request.
 *
 * @since 0.4.0
 */
trait WithDataCachingTrait
{
    /**
     * In-memory cache used when no PSR-16 cache is configured.
     *
     * @since 0.4.0
     *
     * @var array<string, mixed>
     */
    private array $localCache = [];
    /**
     * Gets the cache key suffixes managed by this object.
     *
     * @since 0.4.0
     *
     * @return list<string> The cache key suffixes.
     */
    abstract protected function getCachedKeys(): array;
    /**
     * Gets the base cache key for this object.
     *
     * The base cache key is used as a prefix for all cache keys managed by this object.
     * It should be unique to the implementing class to avoid cache key collisions.
     *
     * @since 0.4.0
     *
     * @return string The base cache key.
     */
    abstract protected function getBaseCacheKey(): string;
    /**
     * Checks if a value exists in the cache.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix (will be appended to the base key).
     * @return bool True if the value exists in cache, false otherwise.
     */
    protected function hasCache(string $key): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->has($fullKey);
        }
        return array_key_exists($fullKey, $this->localCache);
    }
    /**
     * Gets a value from the cache, or computes and caches it if not present.
     *
     * @since 0.4.0
     *
     * @param string                 $key      The cache key suffix (will be appended to the base key).
     * @param callable               $callback The callback to compute the value if not cached.
     * @param int|\DateInterval|null $ttl      The TTL for the cache entry, or null for default.
     *                                         Ignored for local cache.
     * @return mixed The cached or computed value.
     */
    protected function cached(string $key, callable $callback, $ttl = null)
    {
        if ($this->hasCache($key)) {
            return $this->getCache($key);
        }
        $value = $callback();
        $this->setCache($key, $value, $ttl);
        return $value;
    }
    /**
     * Gets a value from the cache.
     *
     * @since 0.4.0
     *
     * @param string $key     The cache key suffix (will be appended to the base key).
     * @param mixed  $default The default value to return if the key does not exist.
     * @return mixed The cached value or the default value if not found.
     */
    protected function getCache(string $key, $default = null)
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->get($fullKey, $default);
        }
        return $this->localCache[$fullKey] ?? $default;
    }
    /**
     * Sets a value in the cache.
     *
     * @since 0.4.0
     *
     * @param string                $key   The cache key suffix (will be appended to the base key).
     * @param mixed                 $value The value to cache.
     * @param int|\DateInterval|null $ttl   The TTL for the cache entry, or null for default. Ignored for local cache.
     * @return bool True on success, false on failure.
     */
    protected function setCache(string $key, $value, $ttl = null): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->set($fullKey, $value, $ttl);
        }
        $this->localCache[$fullKey] = $value;
        return \true;
    }
    /**
     * Invalidates all caches managed by this object.
     *
     * @since 0.4.0
     *
     * @return void
     */
    public function invalidateCaches(): void
    {
        foreach ($this->getCachedKeys() as $key) {
            $this->clearCache($key);
        }
    }
    /**
     * Clears a value from the cache.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix (will be appended to the base key).
     * @return bool True on success, false on failure.
     */
    protected function clearCache(string $key): bool
    {
        $fullKey = $this->buildCacheKey($key);
        $cache = AiClient::getCache();
        if ($cache !== null) {
            return $cache->delete($fullKey);
        }
        unset($this->localCache[$fullKey]);
        return \true;
    }
    /**
     * Builds the full cache key by combining the base key with the suffix.
     *
     * @since 0.4.0
     *
     * @param string $key The cache key suffix.
     * @return string The full cache key.
     */
    private function buildCacheKey(string $key): string
    {
        return $this->getBaseCacheKey() . '_' . $key;
    }
}
              Common/Exception/InvalidArgumentException.php                                                       0000644                 00000000747 15227644447 0015442 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

use WordPress\AiClient\Common\Contracts\AiClientExceptionInterface;
/**
 * Exception thrown when an invalid argument is provided.
 *
 * This extends PHP's built-in InvalidArgumentException while implementing
 * the AI Client exception interface for consistent catch handling.
 *
 * @since 0.2.0
 */
class InvalidArgumentException extends \InvalidArgumentException implements AiClientExceptionInterface
{
}
                         Common/Exception/RuntimeException.php                                                               0000644                 00000000675 15227644447 0013774 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

use WordPress\AiClient\Common\Contracts\AiClientExceptionInterface;
/**
 * Exception thrown for runtime errors.
 *
 * This extends PHP's built-in RuntimeException while implementing
 * the AI Client exception interface for consistent catch handling.
 *
 * @since 0.2.0
 */
class RuntimeException extends \RuntimeException implements AiClientExceptionInterface
{
}
                                                                   Common/Exception/TokenLimitReachedException.php                                                     0000644                 00000002616 15227644447 0015701 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common\Exception;

/**
 * Exception thrown when a token limit is reached during prompt fulfillment.
 *
 * Providers should throw this exception when the token usage for a request
 * exceeds the allowed limit, whether that is the model's context window
 * or a configured maximum.
 *
 * @since 1.0.0
 */
class TokenLimitReachedException extends \WordPress\AiClient\Common\Exception\RuntimeException
{
    /**
     * The token limit that was reached, if known.
     *
     * @since 1.0.0
     *
     * @var int|null
     */
    private $maxTokens;
    /**
     * Creates a new TokenLimitReachedException.
     *
     * @since 1.0.0
     *
     * @param string         $message   The exception message.
     * @param int|null       $maxTokens The token limit that was reached, if known.
     * @param \Throwable|null $previous  The previous throwable used for exception chaining.
     */
    public function __construct(string $message = '', ?int $maxTokens = null, ?\Throwable $previous = null)
    {
        parent::__construct($message, 0, $previous);
        $this->maxTokens = $maxTokens;
    }
    /**
     * Returns the token limit that was reached, if known.
     *
     * @since 1.0.0
     *
     * @return int|null The token limit, or null if not provided.
     */
    public function getMaxTokens(): ?int
    {
        return $this->maxTokens;
    }
}
                                                                                                                  Common/Exception/error_log                                                                          0000644                 00000002264 15227644447 0011672 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 16:14:58 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\AiClientExceptionInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/InvalidArgumentException.php:15
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/InvalidArgumentException.php on line 15
[20-Jul-2026 16:14:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/TokenLimitReachedException.php:15
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/TokenLimitReachedException.php on line 15
[20-Jul-2026 16:14:59 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\AiClientExceptionInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/RuntimeException.php:15
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Common/Exception/RuntimeException.php on line 15
                                                                                                                                                                                                                                                                                                                                            Common/AbstractEnum.php                                                                             0000644                 00000026160 15227644447 0011121 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Common;

use BadMethodCallException;
use JsonSerializable;
use ReflectionClass;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
/**
 * Abstract base class for enum-like behavior in PHP 7.4.
 *
 * This class provides enum-like functionality for PHP versions that don't support native enums.
 * Child classes should define uppercase snake_case constants for enum values.
 *
 * @example
 * class PersonEnum extends AbstractEnum {
 *     public const FIRST_NAME = 'first';
 *     public const LAST_NAME = 'last';
 * }
 *
 * // Usage:
 * $enum = PersonEnum::from('first'); // Creates instance with value 'first'
 * $enum = PersonEnum::tryFrom('invalid'); // Returns null
 * $enum = PersonEnum::firstName(); // Creates instance with value 'first'
 * $enum->name; // 'FIRST_NAME'
 * $enum->value; // 'first'
 * $enum->equals('first'); // Returns true
 * $enum->is(PersonEnum::firstName()); // Returns true
 * PersonEnum::cases(); // Returns array of all enum instances
 *
 * @property-read string $value The value of the enum instance.
 * @property-read string $name The name of the enum constant.
 *
 * @since 0.1.0
 */
abstract class AbstractEnum implements JsonSerializable
{
    /**
     * @var string The value of the enum instance.
     */
    private string $value;
    /**
     * @var string The name of the enum constant.
     */
    private string $name;
    /**
     * @var array<string, array<string, string>> Cache for reflection data.
     */
    private static array $cache = [];
    /**
     * @var array<string, array<string, self>> Cache for enum instances.
     */
    private static array $instances = [];
    /**
     * Constructor is private to ensure instances are created through static methods.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @param string $name The constant name.
     */
    final private function __construct(string $value, string $name)
    {
        $this->value = $value;
        $this->name = $name;
    }
    /**
     * Provides read-only access to properties.
     *
     * @since 0.1.0
     *
     * @param string $property The property name.
     * @return mixed The property value.
     * @throws BadMethodCallException If property doesn't exist.
     */
    final public function __get(string $property)
    {
        if ($property === 'value' || $property === 'name') {
            return $this->{$property};
        }
        throw new BadMethodCallException(sprintf('Property %s::%s does not exist', static::class, $property));
    }
    /**
     * Prevents property modification.
     *
     * @since 0.1.0
     *
     * @param string $property The property name.
     * @param mixed $value The value to set.
     * @throws BadMethodCallException Always, as enum properties are read-only.
     */
    final public function __set(string $property, $value): void
    {
        throw new BadMethodCallException(sprintf('Cannot modify property %s::%s - enum properties are read-only', static::class, $property));
    }
    /**
     * Creates an enum instance from a value, throws exception if invalid.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @return static The enum instance.
     * @throws InvalidArgumentException If the value is not valid.
     */
    final public static function from(string $value): self
    {
        $instance = self::tryFrom($value);
        if ($instance === null) {
            throw new InvalidArgumentException(sprintf('%s is not a valid backing value for enum %s', $value, static::class));
        }
        return $instance;
    }
    /**
     * Tries to create an enum instance from a value, returns null if invalid.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @return static|null The enum instance or null.
     */
    final public static function tryFrom(string $value): ?self
    {
        $constants = static::getConstants();
        foreach ($constants as $name => $constantValue) {
            if ($constantValue === $value) {
                return self::getInstance($constantValue, $name);
            }
        }
        return null;
    }
    /**
     * Gets all enum cases.
     *
     * @since 0.1.0
     *
     * @return static[] Array of all enum instances.
     */
    final public static function cases(): array
    {
        $cases = [];
        $constants = static::getConstants();
        foreach ($constants as $name => $value) {
            $cases[] = self::getInstance($value, $name);
        }
        return $cases;
    }
    /**
     * Checks if this enum has the same value as the given value.
     *
     * @since 0.1.0
     *
     * @param string|self $other The value or enum to compare.
     * @return bool True if values are equal.
     */
    final public function equals($other): bool
    {
        if ($other instanceof self) {
            return $this->is($other);
        }
        return $this->value === $other;
    }
    /**
     * Checks if this enum is the same instance type and value as another enum.
     *
     * @since 0.1.0
     *
     * @param self $other The other enum to compare.
     * @return bool True if enums are identical.
     */
    final public function is(self $other): bool
    {
        return $this === $other;
        // Since we're using singletons, we can use identity comparison
    }
    /**
     * Gets all valid values for this enum.
     *
     * @since 0.1.0
     *
     * @return string[] List of all enum values.
     */
    final public static function getValues(): array
    {
        return array_values(static::getConstants());
    }
    /**
     * Checks if a value is valid for this enum.
     *
     * @since 0.1.0
     *
     * @param string $value The value to check.
     * @return bool True if value is valid.
     */
    final public static function isValidValue(string $value): bool
    {
        return in_array($value, self::getValues(), \true);
    }
    /**
     * Gets or creates a singleton instance for the given value and name.
     *
     * @since 0.1.0
     *
     * @param string $value The enum value.
     * @param string $name The constant name.
     * @return static The enum instance.
     */
    private static function getInstance(string $value, string $name): self
    {
        $className = static::class;
        if (!isset(self::$instances[$className])) {
            self::$instances[$className] = [];
        }
        if (!isset(self::$instances[$className][$name])) {
            $instance = new $className($value, $name);
            self::$instances[$className][$name] = $instance;
        }
        /** @var static */
        return self::$instances[$className][$name];
    }
    /**
     * Gets all constants for this enum class.
     *
     * @since 0.1.0
     *
     * @return array<string, string> Map of constant names to values.
     * @throws RuntimeException If invalid constant found.
     */
    final protected static function getConstants(): array
    {
        $className = static::class;
        if (!isset(self::$cache[$className])) {
            self::$cache[$className] = static::determineClassEnumerations($className);
        }
        return self::$cache[$className];
    }
    /**
     * Determines the class enumerations by reflecting on class constants.
     *
     * This method can be overridden by subclasses to customize how
     * enumerations are determined (e.g., to add dynamic constants).
     *
     * @since 0.1.0
     *
     * @param class-string $className The fully qualified class name.
     * @return array<string, string> Map of constant names to values.
     * @throws RuntimeException If invalid constant found.
     */
    protected static function determineClassEnumerations(string $className): array
    {
        $reflection = new ReflectionClass($className);
        $constants = $reflection->getConstants();
        // Validate all constants
        $enumConstants = [];
        foreach ($constants as $name => $value) {
            // Check if constant name follows uppercase snake_case pattern
            if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $name)) {
                throw new RuntimeException(sprintf('Invalid enum constant name "%s" in %s. Constants must be UPPER_SNAKE_CASE.', $name, $className));
            }
            // Check if value is valid type
            if (!is_string($value)) {
                throw new RuntimeException(sprintf('Invalid enum value type for constant %s::%s. ' . 'Only string values are allowed, %s given.', $className, $name, gettype($value)));
            }
            $enumConstants[$name] = $value;
        }
        return $enumConstants;
    }
    /**
     * Handles dynamic method calls for enum checking.
     *
     * @since 0.1.0
     *
     * @param string $name The method name.
     * @param array<mixed> $arguments The method arguments.
     * @return bool True if the enum value matches.
     * @throws BadMethodCallException If the method doesn't exist.
     */
    final public function __call(string $name, array $arguments): bool
    {
        // Handle is* methods
        if (str_starts_with($name, 'is')) {
            $constantName = self::camelCaseToConstant(substr($name, 2));
            $constants = static::getConstants();
            if (isset($constants[$constantName])) {
                return $this->value === $constants[$constantName];
            }
        }
        throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
    }
    /**
     * Handles static method calls for enum creation.
     *
     * @since 0.1.0
     *
     * @param string $name The method name.
     * @param array<mixed> $arguments The method arguments.
     * @return static The enum instance.
     * @throws BadMethodCallException If the method doesn't exist.
     */
    final public static function __callStatic(string $name, array $arguments): self
    {
        $constantName = self::camelCaseToConstant($name);
        $constants = static::getConstants();
        if (isset($constants[$constantName])) {
            return self::getInstance($constants[$constantName], $constantName);
        }
        throw new BadMethodCallException(sprintf('Method %s::%s does not exist', static::class, $name));
    }
    /**
     * Converts camelCase to CONSTANT_CASE.
     *
     * @since 0.1.0
     *
     * @param string $camelCase The camelCase string.
     * @return string The CONSTANT_CASE version.
     */
    private static function camelCaseToConstant(string $camelCase): string
    {
        $snakeCase = preg_replace('/([a-z])([A-Z])/', '$1_$2', $camelCase);
        if ($snakeCase === null) {
            return strtoupper($camelCase);
        }
        return strtoupper($snakeCase);
    }
    /**
     * Returns string representation of the enum.
     *
     * @since 0.1.0
     *
     * @return string The enum value.
     */
    final public function __toString(): string
    {
        return $this->value;
    }
    /**
     * Converts the enum to a JSON-serializable format.
     *
     * @since 0.1.0
     *
     * @return string The enum value.
     */
    #[\ReturnTypeWillChange]
    public function jsonSerialize()
    {
        return $this->value;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                AiClient.php                                                                                        0000644                 00000041720 15227644447 0006770 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient;

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
use WordPress\AiClientDependencies\Psr\SimpleCache\CacheInterface;
use WordPress\AiClient\Builders\PromptBuilder;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Main AI Client class providing both fluent and traditional APIs for AI operations.
 *
 * This class serves as the primary entry point for AI operations, offering:
 * - Fluent API for easy-to-read chained method calls
 * - Traditional API for array-based configuration (WordPress style)
 * - Integration with provider registry for model discovery
 * - Support for three model specification approaches
 *
 * All model requirements analysis and capability matching is handled
 * automatically by the PromptBuilder, which provides intelligent model
 * discovery based on prompt content and configuration.
 *
 * ## Model Specification Approaches
 *
 * ### 1. Specific Model Instance
 * Use a specific ModelInterface instance when you know exactly which model to use:
 * ```php
 * $model = $registry->getProvider('openai')->getModel('gpt-4');
 * $result = AiClient::generateTextResult('What is PHP?', $model);
 * ```
 *
 * ### 2. ModelConfig for Auto-Discovery
 * Use ModelConfig to specify requirements and let the system discover the best model:
 * ```php
 * $config = new ModelConfig();
 * $config->setTemperature(0.7);
 * $config->setMaxTokens(150);
 *
 * $result = AiClient::generateTextResult('What is PHP?', $config);
 * ```
 *
 * ### 3. Automatic Discovery (Default)
 * Pass null or omit the parameter for intelligent model discovery based on prompt content:
 * ```php
 * // System analyzes prompt and selects appropriate model automatically
 * $result = AiClient::generateTextResult('What is PHP?');
 * $imageResult = AiClient::generateImageResult('A sunset over mountains');
 * ```
 *
 * ## Fluent API Examples
 * ```php
 * // Fluent API with automatic model discovery
 * $result = AiClient::prompt('Generate an image of a sunset')
 *     ->usingTemperature(0.7)
 *     ->generateImageResult();
 *
 * // Fluent API with specific model
 * $result = AiClient::prompt('What is PHP?')
 *     ->usingModel($specificModel)
 *     ->usingTemperature(0.5)
 *     ->generateTextResult();
 *
 * // Fluent API with model configuration
 * $result = AiClient::prompt('Explain quantum physics')
 *     ->usingModelConfig($config)
 *     ->generateTextResult();
 * ```
 *
 * @since 0.1.0
 *
 * @phpstan-import-type Prompt from PromptBuilder
 *
 * phpcs:ignore Generic.Files.LineLength.TooLong
 */
class AiClient
{
    /**
     * @var string The version of the AI Client.
     */
    public const VERSION = '1.3.1';
    /**
     * @var ProviderRegistry|null The default provider registry instance.
     */
    private static ?ProviderRegistry $defaultRegistry = null;
    /**
     * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
     */
    private static ?EventDispatcherInterface $eventDispatcher = null;
    /**
     * @var CacheInterface|null The PSR-16 cache for storing and retrieving cached data.
     */
    private static ?CacheInterface $cache = null;
    /**
     * Gets the default provider registry instance.
     *
     * @since 0.1.0
     *
     * @return ProviderRegistry The default provider registry.
     */
    public static function defaultRegistry(): ProviderRegistry
    {
        if (self::$defaultRegistry === null) {
            self::$defaultRegistry = new ProviderRegistry();
        }
        return self::$defaultRegistry;
    }
    /**
     * Sets the event dispatcher for prompt lifecycle events.
     *
     * The event dispatcher will be used to dispatch BeforeGenerateResultEvent and
     * AfterGenerateResultEvent during prompt generation.
     *
     * @since 0.4.0
     *
     * @param EventDispatcherInterface|null $dispatcher The event dispatcher, or null to disable.
     * @return void
     */
    public static function setEventDispatcher(?EventDispatcherInterface $dispatcher): void
    {
        self::$eventDispatcher = $dispatcher;
    }
    /**
     * Gets the event dispatcher for prompt lifecycle events.
     *
     * @since 0.4.0
     *
     * @return EventDispatcherInterface|null The event dispatcher, or null if not set.
     */
    public static function getEventDispatcher(): ?EventDispatcherInterface
    {
        return self::$eventDispatcher;
    }
    /**
     * Sets the PSR-16 cache for storing and retrieving cached data.
     *
     * The cache can be used to store AI responses and other data to avoid
     * redundant API calls and improve performance.
     *
     * @since 0.4.0
     *
     * @param CacheInterface|null $cache The PSR-16 cache instance, or null to disable caching.
     * @return void
     */
    public static function setCache(?CacheInterface $cache): void
    {
        self::$cache = $cache;
    }
    /**
     * Gets the PSR-16 cache instance.
     *
     * @since 0.4.0
     *
     * @return CacheInterface|null The cache instance, or null if not set.
     */
    public static function getCache(): ?CacheInterface
    {
        return self::$cache;
    }
    /**
     * Checks if a provider is configured and available for use.
     *
     * Supports multiple input formats for developer convenience:
     * - ProviderAvailabilityInterface: Direct availability check
     * - string (provider ID): e.g., AiClient::isConfigured('openai')
     * - string (class name): e.g., AiClient::isConfigured(OpenAiProvider::class)
     *
     * When using string input, this method leverages the ProviderRegistry's centralized
     * dependency management, ensuring HttpTransporter and authentication are properly
     * injected into availability instances.
     *
     * @since 0.1.0
     * @since 0.2.0 Now supports being passed a provider ID or class name.
     *
     * @param ProviderAvailabilityInterface|string|class-string<ProviderInterface> $availabilityOrIdOrClassName
     *        The provider availability instance, provider ID, or provider class name.
     * @return bool True if the provider is configured and available, false otherwise.
     */
    public static function isConfigured($availabilityOrIdOrClassName): bool
    {
        // Handle direct ProviderAvailabilityInterface (backward compatibility)
        if ($availabilityOrIdOrClassName instanceof ProviderAvailabilityInterface) {
            return $availabilityOrIdOrClassName->isConfigured();
        }
        // Handle string input (provider ID or class name) via registry
        if (is_string($availabilityOrIdOrClassName)) {
            return self::defaultRegistry()->isProviderConfigured($availabilityOrIdOrClassName);
        }
        throw new \InvalidArgumentException('Parameter must be a ProviderAvailabilityInterface instance, provider ID string, or provider class name. ' . sprintf('Received: %s', is_object($availabilityOrIdOrClassName) ? get_class($availabilityOrIdOrClassName) : gettype($availabilityOrIdOrClassName)));
    }
    /**
     * Creates a new prompt builder for fluent API usage.
     *
     * Returns a PromptBuilder instance configured with the specified or default registry.
     * The traditional API methods in this class delegate to PromptBuilder
     * for all generation logic.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt Optional initial prompt content.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return PromptBuilder The prompt builder instance.
     */
    public static function prompt($prompt = null, ?ProviderRegistry $registry = null): PromptBuilder
    {
        return new PromptBuilder($registry ?? self::defaultRegistry(), $prompt, self::$eventDispatcher);
    }
    /**
     * Generates content using a unified API that automatically detects model capabilities.
     *
     * When no model is provided, this method delegates to PromptBuilder for intelligent
     * model discovery based on prompt content and configuration. When a model is provided,
     * it infers the capability from the model's interfaces and delegates to the capability-based method.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig $modelOrConfig Specific model to use, or model configuration
     *                                                  for auto-discovery.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the provided model doesn't support any known generation type.
     * @throws \RuntimeException If no suitable model can be found for the prompt.
     */
    public static function generateResult($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateResult();
    }
    /**
     * Generates text using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateTextResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateTextResult();
    }
    /**
     * Generates an image using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateImageResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateImageResult();
    }
    /**
     * Converts text to speech using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function convertTextToSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->convertTextToSpeechResult();
    }
    /**
     * Generates speech using the traditional API approach.
     *
     * @since 0.1.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateSpeechResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateSpeechResult();
    }
    /**
     * Generates a video using the traditional API approach.
     *
     * @since 1.3.0
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig Optional specific model to use,
     *                                                        or model configuration for auto-discovery,
     *                                                        or null for defaults.
     * @param ProviderRegistry|null $registry Optional custom registry. If null, uses default.
     * @return GenerativeAiResult The generation result.
     *
     * @throws \InvalidArgumentException If the prompt format is invalid.
     * @throws \RuntimeException If no suitable model is found.
     */
    public static function generateVideoResult($prompt, $modelOrConfig = null, ?ProviderRegistry $registry = null): GenerativeAiResult
    {
        self::validateModelOrConfigParameter($modelOrConfig);
        return self::getConfiguredPromptBuilder($prompt, $modelOrConfig, $registry)->generateVideoResult();
    }
    /**
     * Creates a new message builder for fluent API usage.
     *
     * This method will be implemented once MessageBuilder is available.
     * MessageBuilder will provide a fluent interface for constructing complex
     * messages with multiple parts, attachments, and metadata.
     *
     * @since 0.1.0
     *
     * @param string|null $text Optional initial message text.
     * @return object MessageBuilder instance (type will be updated when MessageBuilder is available).
     *
     * @throws \RuntimeException When MessageBuilder is not yet available.
     */
    public static function message(?string $text = null)
    {
        throw new RuntimeException('MessageBuilder is not yet available. This method depends on builder infrastructure. ' . 'Use direct generation methods (generateTextResult, generateImageResult, etc.) for now.');
    }
    /**
     * Validates that parameter is ModelInterface, ModelConfig, or null.
     *
     * @param mixed $modelOrConfig The parameter to validate.
     * @return void
     * @throws \InvalidArgumentException If parameter is invalid type.
     */
    private static function validateModelOrConfigParameter($modelOrConfig): void
    {
        if ($modelOrConfig !== null && !$modelOrConfig instanceof ModelInterface && !$modelOrConfig instanceof ModelConfig) {
            throw new InvalidArgumentException('Parameter must be a ModelInterface instance (specific model), ' . 'ModelConfig instance (for auto-discovery), or null (default auto-discovery). ' . sprintf('Received: %s', is_object($modelOrConfig) ? get_class($modelOrConfig) : gettype($modelOrConfig)));
        }
    }
    /**
     * Configures PromptBuilder based on model/config parameter type.
     *
     * @param Prompt $prompt The prompt content.
     * @param ModelInterface|ModelConfig|null $modelOrConfig The model or config parameter.
     * @param ProviderRegistry|null $registry Optional custom registry to use.
     * @return PromptBuilder Configured prompt builder.
     */
    private static function getConfiguredPromptBuilder($prompt, $modelOrConfig, ?ProviderRegistry $registry = null): PromptBuilder
    {
        $builder = self::prompt($prompt, $registry);
        if ($modelOrConfig instanceof ModelInterface) {
            $builder->usingModel($modelOrConfig);
        } elseif ($modelOrConfig instanceof ModelConfig) {
            $builder->usingModelConfig($modelOrConfig);
        }
        // null case: use default model discovery
        return $builder;
    }
}
                                                Messages/Enums/ModalityEnum.php                                                                     0000644                 00000002407 15227644447 0012544 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for input/output modalities.
 *
 * @since 0.1.0
 *
 * @method static self text() Creates an instance for TEXT modality.
 * @method static self document() Creates an instance for DOCUMENT modality.
 * @method static self image() Creates an instance for IMAGE modality.
 * @method static self audio() Creates an instance for AUDIO modality.
 * @method static self video() Creates an instance for VIDEO modality.
 * @method bool isText() Checks if the modality is TEXT.
 * @method bool isDocument() Checks if the modality is DOCUMENT.
 * @method bool isImage() Checks if the modality is IMAGE.
 * @method bool isAudio() Checks if the modality is AUDIO.
 * @method bool isVideo() Checks if the modality is VIDEO.
 */
class ModalityEnum extends AbstractEnum
{
    /**
     * Text modality.
     */
    public const TEXT = 'text';
    /**
     * Document modality (PDFs, Word docs, etc.).
     */
    public const DOCUMENT = 'document';
    /**
     * Image modality.
     */
    public const IMAGE = 'image';
    /**
     * Audio modality.
     */
    public const AUDIO = 'audio';
    /**
     * Video modality.
     */
    public const VIDEO = 'video';
}
                                                                                                                                                                                                                                                         Messages/Enums/MessagePartChannelEnum.php                                                           0000644                 00000001264 15227644447 0014466 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message part channels.
 *
 * @since 0.1.0
 *
 * @method static self content() Creates an instance for CONTENT channel.
 * @method static self thought() Creates an instance for THOUGHT channel.
 * @method bool isContent() Checks if the channel is CONTENT.
 * @method bool isThought() Checks if the channel is THOUGHT.
 */
class MessagePartChannelEnum extends AbstractEnum
{
    /**
     * Regular (primary) content.
     */
    public const CONTENT = 'content';
    /**
     * Model thinking or reasoning.
     */
    public const THOUGHT = 'thought';
}
                                                                                                                                                                                                                                                                                                                                            Messages/Enums/MessagePartTypeEnum.php                                                              0000644                 00000002171 15227644447 0014035 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message part types.
 *
 * @since 0.1.0
 *
 * @method static self text() Creates an instance for TEXT type.
 * @method static self file() Creates an instance for FILE type.
 * @method static self functionCall() Creates an instance for FUNCTION_CALL type.
 * @method static self functionResponse() Creates an instance for FUNCTION_RESPONSE type.
 * @method bool isText() Checks if the type is TEXT.
 * @method bool isFile() Checks if the type is FILE.
 * @method bool isFunctionCall() Checks if the type is FUNCTION_CALL.
 * @method bool isFunctionResponse() Checks if the type is FUNCTION_RESPONSE.
 */
class MessagePartTypeEnum extends AbstractEnum
{
    /**
     * Text content.
     */
    public const TEXT = 'text';
    /**
     * File content (inline or remote).
     */
    public const FILE = 'file';
    /**
     * Function call request.
     */
    public const FUNCTION_CALL = 'function_call';
    /**
     * Function response.
     */
    public const FUNCTION_RESPONSE = 'function_response';
}
                                                                                                                                                                                                                                                                                                                                                                                                       Messages/Enums/error_log                                                                            0000644                 00000005540 15227644447 0011342 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [18-Jul-2026 17:18:54 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessageRoleEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessageRoleEnum.php on line 17
[18-Jul-2026 17:18:57 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/ModalityEnum.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/ModalityEnum.php on line 23
[18-Jul-2026 17:18:57 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartChannelEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartChannelEnum.php on line 17
[18-Jul-2026 17:19:08 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartTypeEnum.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartTypeEnum.php on line 21
[19-Jul-2026 23:50:53 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessageRoleEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessageRoleEnum.php on line 17
[19-Jul-2026 23:50:53 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/ModalityEnum.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/ModalityEnum.php on line 23
[19-Jul-2026 23:50:53 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartChannelEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartChannelEnum.php on line 17
[19-Jul-2026 23:50:54 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartTypeEnum.php:21
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/Enums/MessagePartTypeEnum.php on line 21
                                                                                                                                                                Messages/Enums/MessageRoleEnum.php                                                                  0000644                 00000001244 15227644447 0013166 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for message roles in AI conversations.
 *
 * @since 0.1.0
 *
 * @method static self user() Creates an instance for USER role.
 * @method static self model() Creates an instance for MODEL role.
 * @method bool isUser() Checks if the role is USER.
 * @method bool isModel() Checks if the role is MODEL.
 */
class MessageRoleEnum extends AbstractEnum
{
    /**
     * User role - messages from the user.
     */
    public const USER = 'user';
    /**
     * Model role - messages from the AI model.
     */
    public const MODEL = 'model';
}
                                                                                                                                                                                                                                                                                                                                                            Messages/DTO/MessagePart.php                                                                        0000644                 00000025055 15227644447 0011713 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\Enums\MessagePartChannelEnum;
use WordPress\AiClient\Messages\Enums\MessagePartTypeEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
/**
 * Represents a part of a message.
 *
 * Messages can contain multiple parts of different types, such as text, files,
 * function calls, etc. This DTO encapsulates one such part.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type FileArrayShape from File
 * @phpstan-import-type FunctionCallArrayShape from FunctionCall
 * @phpstan-import-type FunctionResponseArrayShape from FunctionResponse
 *
 * @phpstan-type MessagePartArrayShape array{
 *     channel: string,
 *     type: string,
 *     thoughtSignature?: string,
 *     text?: string,
 *     file?: FileArrayShape,
 *     functionCall?: FunctionCallArrayShape,
 *     functionResponse?: FunctionResponseArrayShape
 * }
 *
 * @extends AbstractDataTransferObject<MessagePartArrayShape>
 */
class MessagePart extends AbstractDataTransferObject
{
    public const KEY_CHANNEL = 'channel';
    public const KEY_TYPE = 'type';
    public const KEY_THOUGHT_SIGNATURE = 'thoughtSignature';
    public const KEY_TEXT = 'text';
    public const KEY_FILE = 'file';
    public const KEY_FUNCTION_CALL = 'functionCall';
    public const KEY_FUNCTION_RESPONSE = 'functionResponse';
    /**
     * @var MessagePartChannelEnum The channel this message part belongs to.
     */
    private MessagePartChannelEnum $channel;
    /**
     * @var MessagePartTypeEnum The type of this message part.
     */
    private MessagePartTypeEnum $type;
    /**
     * @var string|null Thought signature for extended thinking.
     */
    private ?string $thoughtSignature = null;
    /**
     * @var string|null Text content (when type is TEXT).
     */
    private ?string $text = null;
    /**
     * @var File|null File data (when type is FILE).
     */
    private ?File $file = null;
    /**
     * @var FunctionCall|null Function call request (when type is FUNCTION_CALL).
     */
    private ?FunctionCall $functionCall = null;
    /**
     * @var FunctionResponse|null Function response (when type is FUNCTION_RESPONSE).
     */
    private ?FunctionResponse $functionResponse = null;
    /**
     * Constructor that accepts various content types and infers the message part type.
     *
     * @since 0.1.0
     *
     * @param mixed $content The content of this message part.
     * @param MessagePartChannelEnum|null $channel The channel this part belongs to. Defaults to CONTENT.
     * @param string|null $thoughtSignature Optional thought signature for extended thinking.
     * @throws InvalidArgumentException If an unsupported content type is provided.
     */
    public function __construct($content, ?MessagePartChannelEnum $channel = null, ?string $thoughtSignature = null)
    {
        $this->channel = $channel ?? MessagePartChannelEnum::content();
        $this->thoughtSignature = $thoughtSignature;
        if (is_string($content)) {
            $this->type = MessagePartTypeEnum::text();
            $this->text = $content;
        } elseif ($content instanceof File) {
            $this->type = MessagePartTypeEnum::file();
            $this->file = $content;
        } elseif ($content instanceof FunctionCall) {
            $this->type = MessagePartTypeEnum::functionCall();
            $this->functionCall = $content;
        } elseif ($content instanceof FunctionResponse) {
            $this->type = MessagePartTypeEnum::functionResponse();
            $this->functionResponse = $content;
        } else {
            $type = is_object($content) ? get_class($content) : gettype($content);
            throw new InvalidArgumentException(sprintf('Unsupported content type %s. Expected string, File, ' . 'FunctionCall, or FunctionResponse.', $type));
        }
    }
    /**
     * Gets the channel this message part belongs to.
     *
     * @since 0.1.0
     *
     * @return MessagePartChannelEnum The channel.
     */
    public function getChannel(): MessagePartChannelEnum
    {
        return $this->channel;
    }
    /**
     * Gets the type of this message part.
     *
     * @since 0.1.0
     *
     * @return MessagePartTypeEnum The type.
     */
    public function getType(): MessagePartTypeEnum
    {
        return $this->type;
    }
    /**
     * Gets the thought signature.
     *
     * @since 1.3.0
     *
     * @return string|null The thought signature or null if not set.
     */
    public function getThoughtSignature(): ?string
    {
        return $this->thoughtSignature;
    }
    /**
     * Gets the text content.
     *
     * @since 0.1.0
     *
     * @return string|null The text content or null if not a text part.
     */
    public function getText(): ?string
    {
        return $this->text;
    }
    /**
     * Gets the file.
     *
     * @since 0.1.0
     *
     * @return File|null The file or null if not a file part.
     */
    public function getFile(): ?File
    {
        return $this->file;
    }
    /**
     * Gets the function call.
     *
     * @since 0.1.0
     *
     * @return FunctionCall|null The function call or null if not a function call part.
     */
    public function getFunctionCall(): ?FunctionCall
    {
        return $this->functionCall;
    }
    /**
     * Gets the function response.
     *
     * @since 0.1.0
     *
     * @return FunctionResponse|null The function response or null if not a function response part.
     */
    public function getFunctionResponse(): ?FunctionResponse
    {
        return $this->functionResponse;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        $channelSchema = ['type' => 'string', 'enum' => MessagePartChannelEnum::getValues(), 'description' => 'The channel this message part belongs to.'];
        $thoughtSignatureSchema = ['type' => 'string', 'description' => 'Thought signature for extended thinking.'];
        return ['oneOf' => [['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::text()->value], self::KEY_TEXT => ['type' => 'string', 'description' => 'Text content.'], self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_TEXT], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::file()->value], self::KEY_FILE => File::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FILE], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionCall()->value], self::KEY_FUNCTION_CALL => FunctionCall::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_CALL], 'additionalProperties' => \false], ['type' => 'object', 'properties' => [self::KEY_CHANNEL => $channelSchema, self::KEY_TYPE => ['type' => 'string', 'const' => MessagePartTypeEnum::functionResponse()->value], self::KEY_FUNCTION_RESPONSE => FunctionResponse::getJsonSchema(), self::KEY_THOUGHT_SIGNATURE => $thoughtSignatureSchema], 'required' => [self::KEY_TYPE, self::KEY_FUNCTION_RESPONSE], 'additionalProperties' => \false]]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return MessagePartArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_CHANNEL => $this->channel->value, self::KEY_TYPE => $this->type->value];
        if ($this->text !== null) {
            $data[self::KEY_TEXT] = $this->text;
        } elseif ($this->file !== null) {
            $data[self::KEY_FILE] = $this->file->toArray();
        } elseif ($this->functionCall !== null) {
            $data[self::KEY_FUNCTION_CALL] = $this->functionCall->toArray();
        } elseif ($this->functionResponse !== null) {
            $data[self::KEY_FUNCTION_RESPONSE] = $this->functionResponse->toArray();
        } else {
            throw new RuntimeException('MessagePart requires one of: text, file, functionCall, or functionResponse. ' . 'This should not be a possible condition.');
        }
        if ($this->thoughtSignature !== null) {
            $data[self::KEY_THOUGHT_SIGNATURE] = $this->thoughtSignature;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        if (isset($array[self::KEY_CHANNEL])) {
            $channel = MessagePartChannelEnum::from($array[self::KEY_CHANNEL]);
        } else {
            $channel = null;
        }
        $thoughtSignature = $array[self::KEY_THOUGHT_SIGNATURE] ?? null;
        // Check which properties are set to determine how to construct the MessagePart
        if (isset($array[self::KEY_TEXT])) {
            return new self($array[self::KEY_TEXT], $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FILE])) {
            return new self(File::fromArray($array[self::KEY_FILE]), $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FUNCTION_CALL])) {
            return new self(FunctionCall::fromArray($array[self::KEY_FUNCTION_CALL]), $channel, $thoughtSignature);
        } elseif (isset($array[self::KEY_FUNCTION_RESPONSE])) {
            return new self(FunctionResponse::fromArray($array[self::KEY_FUNCTION_RESPONSE]), $channel, $thoughtSignature);
        } else {
            throw new InvalidArgumentException('MessagePart requires one of: text, file, functionCall, or functionResponse.');
        }
    }
    /**
     * Performs a deep clone of the message part.
     *
     * This method ensures that nested objects (file, function call, function response)
     * are cloned to prevent modifications to the cloned part from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        if ($this->file !== null) {
            $this->file = clone $this->file;
        }
        if ($this->functionCall !== null) {
            $this->functionCall = clone $this->functionCall;
        }
        if ($this->functionResponse !== null) {
            $this->functionResponse = clone $this->functionResponse;
        }
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   Messages/DTO/UserMessage.php                                                                        0000644                 00000001454 15227644447 0011720 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message from a user.
 *
 * This is a convenience class that automatically sets the role to USER.
 *
 * Important: Do not rely on `instanceof UserMessage` to determine the message role.
 * This is merely a helper class for construction. Always use `$message->getRole()`
 * to check the role of a message.
 *
 * @since 0.1.0
 */
class UserMessage extends \WordPress\AiClient\Messages\DTO\Message
{
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessagePart[] $parts The parts that make up this message.
     */
    public function __construct(array $parts)
    {
        parent::__construct(MessageRoleEnum::user(), $parts);
    }
}
                                                                                                                                                                                                                    Messages/DTO/ModelMessage.php                                                                       0000644                 00000001544 15227644447 0012042 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message from the AI model.
 *
 * This is a convenience class that automatically sets the role to MODEL.
 * Model messages contain the AI's responses.
 *
 * Important: Do not rely on `instanceof ModelMessage` to determine the message role.
 * This is merely a helper class for construction. Always use `$message->getRole()`
 * to check the role of a message.
 *
 * @since 0.1.0
 */
class ModelMessage extends \WordPress\AiClient\Messages\DTO\Message
{
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessagePart[] $parts The parts that make up this message.
     */
    public function __construct(array $parts)
    {
        parent::__construct(MessageRoleEnum::model(), $parts);
    }
}
                                                                                                                                                            Messages/DTO/error_log                                                                              0000644                 00000005420 15227644447 0010676 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 05:18:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/Message.php:26
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/Message.php on line 26
[19-Jul-2026 14:35:37 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Messages\DTO\Message" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php on line 18
[19-Jul-2026 14:35:39 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php:38
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php on line 38
[19-Jul-2026 14:35:43 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Messages\DTO\Message" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php on line 19
[20-Jul-2026 08:44:35 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/Message.php:26
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/Message.php on line 26
[20-Jul-2026 17:54:37 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Messages\DTO\Message" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/UserMessage.php on line 18
[20-Jul-2026 17:54:37 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php:38
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php on line 38
[20-Jul-2026 17:54:38 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Messages\DTO\Message" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Messages/DTO/ModelMessage.php on line 19
                                                                                                                                                                                                                                                Messages/DTO/Message.php                                                                            0000644                 00000013044 15227644447 0011057 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Messages\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
/**
 * Represents a message in an AI conversation.
 *
 * Messages are the fundamental unit of communication with AI models,
 * containing a role and one or more parts with different content types.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type MessageArrayShape array{
 *     role: string,
 *     parts: array<MessagePartArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<MessageArrayShape>
 */
class Message extends AbstractDataTransferObject
{
    public const KEY_ROLE = 'role';
    public const KEY_PARTS = 'parts';
    /**
     * @var MessageRoleEnum The role of the message sender.
     */
    protected MessageRoleEnum $role;
    /**
     * @var MessagePart[] The parts that make up this message.
     */
    protected array $parts;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param MessageRoleEnum $role The role of the message sender.
     * @param MessagePart[] $parts The parts that make up this message.
     * @throws InvalidArgumentException If parts contain invalid content for the role.
     */
    public function __construct(MessageRoleEnum $role, array $parts)
    {
        $this->role = $role;
        $this->parts = $parts;
        $this->validateParts();
    }
    /**
     * Gets the role of the message sender.
     *
     * @since 0.1.0
     *
     * @return MessageRoleEnum The role.
     */
    public function getRole(): MessageRoleEnum
    {
        return $this->role;
    }
    /**
     * Gets the message parts.
     *
     * @since 0.1.0
     *
     * @return MessagePart[] The message parts.
     */
    public function getParts(): array
    {
        return $this->parts;
    }
    /**
     * Returns a new instance with the given part appended.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The part to append.
     * @return Message A new instance with the part appended.
     * @throws InvalidArgumentException If the part is invalid for the role.
     */
    public function withPart(\WordPress\AiClient\Messages\DTO\MessagePart $part): \WordPress\AiClient\Messages\DTO\Message
    {
        $newParts = $this->parts;
        $newParts[] = $part;
        return new \WordPress\AiClient\Messages\DTO\Message($this->role, $newParts);
    }
    /**
     * Validates that the message parts are appropriate for the message role.
     *
     * @since 0.1.0
     *
     * @return void
     * @throws InvalidArgumentException If validation fails.
     */
    private function validateParts(): void
    {
        foreach ($this->parts as $part) {
            $type = $part->getType();
            if ($this->role->isUser() && $type->isFunctionCall()) {
                throw new InvalidArgumentException('User messages cannot contain function calls.');
            }
            if ($this->role->isModel() && $type->isFunctionResponse()) {
                throw new InvalidArgumentException('Model messages cannot contain function responses.');
            }
        }
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ROLE => ['type' => 'string', 'enum' => MessageRoleEnum::getValues(), 'description' => 'The role of the message sender.'], self::KEY_PARTS => ['type' => 'array', 'items' => \WordPress\AiClient\Messages\DTO\MessagePart::getJsonSchema(), 'minItems' => 1, 'description' => 'The parts that make up this message.']], 'required' => [self::KEY_ROLE, self::KEY_PARTS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return MessageArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ROLE => $this->role->value, self::KEY_PARTS => array_map(function (\WordPress\AiClient\Messages\DTO\MessagePart $part) {
            return $part->toArray();
        }, $this->parts)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return self The specific message class based on the role.
     */
    final public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ROLE, self::KEY_PARTS]);
        $role = MessageRoleEnum::from($array[self::KEY_ROLE]);
        $partsData = $array[self::KEY_PARTS];
        $parts = array_map(function (array $partData) {
            return \WordPress\AiClient\Messages\DTO\MessagePart::fromArray($partData);
        }, $partsData);
        // Determine which concrete class to instantiate based on role
        if ($role->isUser()) {
            return new \WordPress\AiClient\Messages\DTO\UserMessage($parts);
        } elseif ($role->isModel()) {
            return new \WordPress\AiClient\Messages\DTO\ModelMessage($parts);
        } else {
            // Only USER and MODEL roles are supported
            throw new InvalidArgumentException('Invalid message role: ' . $role->value);
        }
    }
    /**
     * Performs a deep clone of the message.
     *
     * This method ensures that message part objects are cloned to prevent
     * modifications to the cloned message from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedParts = [];
        foreach ($this->parts as $part) {
            $clonedParts[] = clone $part;
        }
        $this->parts = $clonedParts;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            Operations/Contracts/OperationInterface.php                                                         0000644                 00000001413 15227644447 0015137 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\Contracts;

use WordPress\AiClient\Operations\Enums\OperationStateEnum;
/**
 * Interface for AI operations.
 *
 * Operations represent long-running AI tasks that may not complete immediately.
 * They provide a way to track the progress and retrieve results asynchronously.
 *
 * @since 0.1.0
 */
interface OperationInterface
{
    /**
     * Gets the operation ID.
     *
     * @since 0.1.0
     *
     * @return string The unique operation identifier.
     */
    public function getId(): string;
    /**
     * Gets the current state of the operation.
     *
     * @since 0.1.0
     *
     * @return OperationStateEnum The operation state.
     */
    public function getState(): OperationStateEnum;
}
                                                                                                                                                                                                                                                     Operations/Enums/OperationStateEnum.php                                                             0000644                 00000002503 15227644447 0014274 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for operation states.
 *
 * @since 0.1.0
 *
 * @method static self starting() Creates an instance for STARTING state.
 * @method static self processing() Creates an instance for PROCESSING state.
 * @method static self succeeded() Creates an instance for SUCCEEDED state.
 * @method static self failed() Creates an instance for FAILED state.
 * @method static self canceled() Creates an instance for CANCELED state.
 * @method bool isStarting() Checks if the state is STARTING.
 * @method bool isProcessing() Checks if the state is PROCESSING.
 * @method bool isSucceeded() Checks if the state is SUCCEEDED.
 * @method bool isFailed() Checks if the state is FAILED.
 * @method bool isCanceled() Checks if the state is CANCELED.
 */
class OperationStateEnum extends AbstractEnum
{
    /**
     * Operation is starting.
     */
    public const STARTING = 'starting';
    /**
     * Operation is processing.
     */
    public const PROCESSING = 'processing';
    /**
     * Operation succeeded.
     */
    public const SUCCEEDED = 'succeeded';
    /**
     * Operation failed.
     */
    public const FAILED = 'failed';
    /**
     * Operation was canceled.
     */
    public const CANCELED = 'canceled';
}
                                                                                                                                                                                             Operations/Enums/error_log                                                                          0000644                 00000000562 15227644447 0011715 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 16:14:58 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Operations/Enums/OperationStateEnum.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Operations/Enums/OperationStateEnum.php on line 23
                                                                                                                                              Operations/DTO/GenerativeAiOperation.php                                                            0000644                 00000012132 15227644447 0014270 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Operations\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Operations\Contracts\OperationInterface;
use WordPress\AiClient\Operations\Enums\OperationStateEnum;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Represents a long-running generative AI operation.
 *
 * This DTO tracks the progress of generative AI tasks that may not complete
 * immediately, providing access to the result once available.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type GenerativeAiResultArrayShape from GenerativeAiResult
 *
 * @phpstan-type GenerativeAiOperationArrayShape array{id: string, state: string, result?: GenerativeAiResultArrayShape}
 *
 * @extends AbstractDataTransferObject<GenerativeAiOperationArrayShape>
 */
class GenerativeAiOperation extends AbstractDataTransferObject implements OperationInterface
{
    public const KEY_ID = 'id';
    public const KEY_STATE = 'state';
    public const KEY_RESULT = 'result';
    /**
     * @var string Unique identifier for this operation.
     */
    private string $id;
    /**
     * @var OperationStateEnum The current state of the operation.
     */
    private OperationStateEnum $state;
    /**
     * @var GenerativeAiResult|null The result once the operation completes.
     */
    private ?GenerativeAiResult $result;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id Unique identifier for this operation.
     * @param OperationStateEnum $state The current state of the operation.
     * @param GenerativeAiResult|null $result The result once the operation completes.
     */
    public function __construct(string $id, OperationStateEnum $state, ?GenerativeAiResult $result = null)
    {
        $this->id = $id;
        $this->state = $state;
        $this->result = $result;
    }
    /**
     * Creates a deep clone of this operation.
     *
     * Clones the result object if present to ensure the cloned
     * operation is independent of the original.
     * The state enum is immutable and can be safely shared.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone the result if present (GenerativeAiResult has __clone)
        if ($this->result !== null) {
            $this->result = clone $this->result;
        }
        // Note: $state is an immutable enum and can be safely shared
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getState(): OperationStateEnum
    {
        return $this->state;
    }
    /**
     * Gets the operation result.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult|null The result or null if not yet complete.
     */
    public function getResult(): ?GenerativeAiResult
    {
        return $this->result;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['oneOf' => [
            // Succeeded state - has result
            ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'const' => OperationStateEnum::succeeded()->value], self::KEY_RESULT => GenerativeAiResult::getJsonSchema()], 'required' => [self::KEY_ID, self::KEY_STATE, self::KEY_RESULT], 'additionalProperties' => \false],
            // All other states - no result
            ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this operation.'], self::KEY_STATE => ['type' => 'string', 'enum' => [OperationStateEnum::starting()->value, OperationStateEnum::processing()->value, OperationStateEnum::failed()->value, OperationStateEnum::canceled()->value], 'description' => 'The current state of the operation.']], 'required' => [self::KEY_ID, self::KEY_STATE], 'additionalProperties' => \false],
        ]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return GenerativeAiOperationArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_ID => $this->id, self::KEY_STATE => $this->state->value];
        if ($this->result !== null) {
            $data[self::KEY_RESULT] = $this->result->toArray();
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_STATE]);
        $state = OperationStateEnum::from($array[self::KEY_STATE]);
        if ($state->isSucceeded()) {
            // If the operation has succeeded, it must have a result
            static::validateFromArrayData($array, [self::KEY_RESULT]);
        }
        $result = null;
        if (isset($array[self::KEY_RESULT])) {
            $result = GenerativeAiResult::fromArray($array[self::KEY_RESULT]);
        }
        return new self($array[self::KEY_ID], $state, $result);
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                      Operations/DTO/error_log                                                                            0000644                 00000000602 15227644447 0011247 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [20-Jul-2026 16:14:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Operations/DTO/GenerativeAiOperation.php:24
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Operations/DTO/GenerativeAiOperation.php on line 24
                                                                                                                              Results/Contracts/ResultInterface.php                                                               0000644                 00000002571 15227644447 0014001 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\Contracts;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Results\DTO\TokenUsage;
/**
 * Interface for AI operation results.
 *
 * Results contain the output from AI operations along with metadata
 * such as token usage and provider-specific information.
 *
 * @since 0.1.0
 */
interface ResultInterface
{
    /**
     * Gets the result ID.
     *
     * @since 0.1.0
     *
     * @return string The unique result identifier.
     */
    public function getId(): string;
    /**
     * Gets token usage information.
     *
     * @since 0.1.0
     *
     * @return TokenUsage Token usage statistics.
     */
    public function getTokenUsage(): TokenUsage;
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProviderMetadata(): ProviderMetadata;
    /**
     * Gets the model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata The model metadata.
     */
    public function getModelMetadata(): ModelMetadata;
    /**
     * Gets provider-specific metadata.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> Provider metadata.
     */
    public function getAdditionalData(): array;
}
                                                                                                                                       Results/Enums/FinishReasonEnum.php                                                                  0000644                 00000002616 15227644447 0013246 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for finish reasons of AI generation.
 *
 * @since 0.1.0
 *
 * @method static self stop() Creates an instance for STOP reason.
 * @method static self length() Creates an instance for LENGTH reason.
 * @method static self contentFilter() Creates an instance for CONTENT_FILTER reason.
 * @method static self toolCalls() Creates an instance for TOOL_CALLS reason.
 * @method static self error() Creates an instance for ERROR reason.
 * @method bool isStop() Checks if the reason is STOP.
 * @method bool isLength() Checks if the reason is LENGTH.
 * @method bool isContentFilter() Checks if the reason is CONTENT_FILTER.
 * @method bool isToolCalls() Checks if the reason is TOOL_CALLS.
 * @method bool isError() Checks if the reason is ERROR.
 */
class FinishReasonEnum extends AbstractEnum
{
    /**
     * Generation stopped naturally.
     */
    public const STOP = 'stop';
    /**
     * Generation stopped due to max length.
     */
    public const LENGTH = 'length';
    /**
     * Generation stopped due to content filter.
     */
    public const CONTENT_FILTER = 'content_filter';
    /**
     * Generation stopped to make tool calls.
     */
    public const TOOL_CALLS = 'tool_calls';
    /**
     * Generation stopped due to error.
     */
    public const ERROR = 'error';
}
                                                                                                                  Results/Enums/error_log                                                                             0000644                 00000001320 15227644447 0011224 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 03:31:47 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Results/Enums/FinishReasonEnum.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Results/Enums/FinishReasonEnum.php on line 23
[20-Jul-2026 06:58:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Results/Enums/FinishReasonEnum.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Results/Enums/FinishReasonEnum.php on line 23
                                                                                                                                                                                                                                                                                                                Results/DTO/TokenUsage.php                                                                          0000644                 00000011420 15227644447 0011426 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
/**
 * Represents token usage statistics for an AI operation.
 *
 * This DTO tracks the number of tokens used in prompts and completions,
 * which is important for monitoring usage and costs.
 *
 * Note that thought tokens are a subset of completion tokens, not additive.
 * In other words: completionTokens - thoughtTokens = tokens of actual output content.
 *
 * @since 0.1.0
 *
 * @phpstan-type TokenUsageArrayShape array{
 *     promptTokens: int,
 *     completionTokens: int,
 *     totalTokens: int,
 *     thoughtTokens?: int
 * }
 *
 * @extends AbstractDataTransferObject<TokenUsageArrayShape>
 */
class TokenUsage extends AbstractDataTransferObject
{
    public const KEY_PROMPT_TOKENS = 'promptTokens';
    public const KEY_COMPLETION_TOKENS = 'completionTokens';
    public const KEY_TOTAL_TOKENS = 'totalTokens';
    public const KEY_THOUGHT_TOKENS = 'thoughtTokens';
    /**
     * @var int Number of tokens in the prompt.
     */
    private int $promptTokens;
    /**
     * @var int Number of tokens in the completion, including any thought tokens.
     */
    private int $completionTokens;
    /**
     * @var int Total number of tokens used.
     */
    private int $totalTokens;
    /**
     * @var int|null Number of tokens used for thinking, as a subset of completion tokens.
     */
    private ?int $thoughtTokens;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param int $promptTokens Number of tokens in the prompt.
     * @param int $completionTokens Number of tokens in the completion, including any thought tokens.
     * @param int $totalTokens Total number of tokens used.
     * @param int|null $thoughtTokens Number of tokens used for thinking, as a subset of completion tokens.
     */
    public function __construct(int $promptTokens, int $completionTokens, int $totalTokens, ?int $thoughtTokens = null)
    {
        $this->promptTokens = $promptTokens;
        $this->completionTokens = $completionTokens;
        $this->totalTokens = $totalTokens;
        $this->thoughtTokens = $thoughtTokens;
    }
    /**
     * Gets the number of prompt tokens.
     *
     * @since 0.1.0
     *
     * @return int The prompt token count.
     */
    public function getPromptTokens(): int
    {
        return $this->promptTokens;
    }
    /**
     * Gets the number of completion tokens, including any thought tokens.
     *
     * @since 0.1.0
     *
     * @return int The completion token count.
     */
    public function getCompletionTokens(): int
    {
        return $this->completionTokens;
    }
    /**
     * Gets the total number of tokens.
     *
     * @since 0.1.0
     *
     * @return int The total token count.
     */
    public function getTotalTokens(): int
    {
        return $this->totalTokens;
    }
    /**
     * Gets the number of thought tokens, which is a subset of the completion token count.
     *
     * @since 1.3.0
     *
     * @return int|null The thought token count or null if not available.
     */
    public function getThoughtTokens(): ?int
    {
        return $this->thoughtTokens;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_PROMPT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the prompt.'], self::KEY_COMPLETION_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens in the completion, including any thought tokens.'], self::KEY_TOTAL_TOKENS => ['type' => 'integer', 'description' => 'Total number of tokens used.'], self::KEY_THOUGHT_TOKENS => ['type' => 'integer', 'description' => 'Number of tokens used for thinking, as a subset of completion tokens.']], 'required' => [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return TokenUsageArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_PROMPT_TOKENS => $this->promptTokens, self::KEY_COMPLETION_TOKENS => $this->completionTokens, self::KEY_TOTAL_TOKENS => $this->totalTokens];
        if ($this->thoughtTokens !== null) {
            $data[self::KEY_THOUGHT_TOKENS] = $this->thoughtTokens;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_PROMPT_TOKENS, self::KEY_COMPLETION_TOKENS, self::KEY_TOTAL_TOKENS]);
        return new self($array[self::KEY_PROMPT_TOKENS], $array[self::KEY_COMPLETION_TOKENS], $array[self::KEY_TOTAL_TOKENS], $array[self::KEY_THOUGHT_TOKENS] ?? null);
    }
}
                                                                                                                                                                                                                                                Results/DTO/GenerativeAiResult.php                                                                  0000644                 00000032757 15227644447 0013143 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Results\Contracts\ResultInterface;
/**
 * Represents the result of a generative AI operation.
 *
 * This DTO contains the generated candidates along with usage statistics
 * and metadata from the AI provider.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type CandidateArrayShape from Candidate
 * @phpstan-import-type TokenUsageArrayShape from TokenUsage
 * @phpstan-import-type ProviderMetadataArrayShape from ProviderMetadata
 * @phpstan-import-type ModelMetadataArrayShape from ModelMetadata
 *
 * @phpstan-type GenerativeAiResultArrayShape array{
 *     id: string,
 *     candidates: array<CandidateArrayShape>,
 *     tokenUsage: TokenUsageArrayShape,
 *     providerMetadata: ProviderMetadataArrayShape,
 *     modelMetadata: ModelMetadataArrayShape,
 *     additionalData?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<GenerativeAiResultArrayShape>
 */
class GenerativeAiResult extends AbstractDataTransferObject implements ResultInterface
{
    public const KEY_ID = 'id';
    public const KEY_CANDIDATES = 'candidates';
    public const KEY_TOKEN_USAGE = 'tokenUsage';
    public const KEY_PROVIDER_METADATA = 'providerMetadata';
    public const KEY_MODEL_METADATA = 'modelMetadata';
    public const KEY_ADDITIONAL_DATA = 'additionalData';
    /**
     * @var string Unique identifier for this result.
     */
    private string $id;
    /**
     * @var Candidate[] The generated candidates.
     */
    private array $candidates;
    /**
     * @var TokenUsage Token usage statistics.
     */
    private \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage;
    /**
     * @var ProviderMetadata Provider metadata.
     */
    private ProviderMetadata $providerMetadata;
    /**
     * @var ModelMetadata Model metadata.
     */
    private ModelMetadata $modelMetadata;
    /**
     * @var array<string, mixed> Additional data.
     */
    private array $additionalData;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id Unique identifier for this result.
     * @param Candidate[] $candidates The generated candidates.
     * @param TokenUsage $tokenUsage Token usage statistics.
     * @param ProviderMetadata $providerMetadata Provider metadata.
     * @param ModelMetadata $modelMetadata Model metadata.
     * @param array<string, mixed> $additionalData Additional data.
     * @throws InvalidArgumentException If no candidates provided.
     */
    public function __construct(string $id, array $candidates, \WordPress\AiClient\Results\DTO\TokenUsage $tokenUsage, ProviderMetadata $providerMetadata, ModelMetadata $modelMetadata, array $additionalData = [])
    {
        if (empty($candidates)) {
            throw new InvalidArgumentException('At least one candidate must be provided');
        }
        $this->id = $id;
        $this->candidates = $candidates;
        $this->tokenUsage = $tokenUsage;
        $this->providerMetadata = $providerMetadata;
        $this->modelMetadata = $modelMetadata;
        $this->additionalData = $additionalData;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the generated candidates.
     *
     * @since 0.1.0
     *
     * @return Candidate[] The candidates.
     */
    public function getCandidates(): array
    {
        return $this->candidates;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getTokenUsage(): \WordPress\AiClient\Results\DTO\TokenUsage
    {
        return $this->tokenUsage;
    }
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProviderMetadata(): ProviderMetadata
    {
        return $this->providerMetadata;
    }
    /**
     * Gets the model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata The model metadata.
     */
    public function getModelMetadata(): ModelMetadata
    {
        return $this->modelMetadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getAdditionalData(): array
    {
        return $this->additionalData;
    }
    /**
     * Gets the total number of candidates.
     *
     * @since 0.1.0
     *
     * @return int The total number of candidates.
     */
    public function getCandidateCount(): int
    {
        return count($this->candidates);
    }
    /**
     * Checks if the result has multiple candidates.
     *
     * @since 0.1.0
     *
     * @return bool True if there are multiple candidates, false otherwise.
     */
    public function hasMultipleCandidates(): bool
    {
        return $this->getCandidateCount() > 1;
    }
    /**
     * Converts the first candidate to text.
     *
     * Only text from the content channel is considered. Text within model thought or reasoning is ignored.
     *
     * @since 0.1.0
     *
     * @return string The text content.
     * @throws RuntimeException If no text content.
     */
    public function toText(): string
    {
        $message = $this->candidates[0]->getMessage();
        foreach ($message->getParts() as $part) {
            $channel = $part->getChannel();
            $text = $part->getText();
            if ($channel->isContent() && $text !== null) {
                return $text;
            }
        }
        throw new RuntimeException('No text content found in first candidate');
    }
    /**
     * Converts the first candidate to a file.
     *
     * Only files from the content channel are considered. Files within model thought or reasoning are ignored.
     *
     * @since 0.1.0
     *
     * @return File The file.
     * @throws RuntimeException If no file content.
     */
    public function toFile(): File
    {
        $message = $this->candidates[0]->getMessage();
        foreach ($message->getParts() as $part) {
            $channel = $part->getChannel();
            $file = $part->getFile();
            if ($channel->isContent() && $file !== null) {
                return $file;
            }
        }
        throw new RuntimeException('No file content found in first candidate');
    }
    /**
     * Converts the first candidate to an image file.
     *
     * @since 0.1.0
     *
     * @return File The image file.
     * @throws RuntimeException If no image content.
     */
    public function toImageFile(): File
    {
        $file = $this->toFile();
        if (!$file->isImage()) {
            throw new RuntimeException(sprintf('File is not an image. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to an audio file.
     *
     * @since 0.1.0
     *
     * @return File The audio file.
     * @throws RuntimeException If no audio content.
     */
    public function toAudioFile(): File
    {
        $file = $this->toFile();
        if (!$file->isAudio()) {
            throw new RuntimeException(sprintf('File is not an audio file. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to a video file.
     *
     * @since 0.1.0
     *
     * @return File The video file.
     * @throws RuntimeException If no video content.
     */
    public function toVideoFile(): File
    {
        $file = $this->toFile();
        if (!$file->isVideo()) {
            throw new RuntimeException(sprintf('File is not a video file. MIME type: %s', $file->getMimeType()));
        }
        return $file;
    }
    /**
     * Converts the first candidate to a message.
     *
     * @since 0.1.0
     *
     * @return Message The message.
     */
    public function toMessage(): Message
    {
        return $this->candidates[0]->getMessage();
    }
    /**
     * Converts all candidates to text.
     *
     * @since 0.1.0
     *
     * @return list<string> Array of text content.
     */
    public function toTexts(): array
    {
        $texts = [];
        foreach ($this->candidates as $candidate) {
            $message = $candidate->getMessage();
            foreach ($message->getParts() as $part) {
                $channel = $part->getChannel();
                $text = $part->getText();
                if ($channel->isContent() && $text !== null) {
                    $texts[] = $text;
                    break;
                }
            }
        }
        return $texts;
    }
    /**
     * Converts all candidates to files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of files.
     */
    public function toFiles(): array
    {
        $files = [];
        foreach ($this->candidates as $candidate) {
            $message = $candidate->getMessage();
            foreach ($message->getParts() as $part) {
                $channel = $part->getChannel();
                $file = $part->getFile();
                if ($channel->isContent() && $file !== null) {
                    $files[] = $file;
                    break;
                }
            }
        }
        return $files;
    }
    /**
     * Converts all candidates to image files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of image files.
     */
    public function toImageFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isImage()));
    }
    /**
     * Converts all candidates to audio files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of audio files.
     */
    public function toAudioFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isAudio()));
    }
    /**
     * Converts all candidates to video files.
     *
     * @since 0.1.0
     *
     * @return list<File> Array of video files.
     */
    public function toVideoFiles(): array
    {
        return array_values(array_filter($this->toFiles(), fn(File $file) => $file->isVideo()));
    }
    /**
     * Converts all candidates to messages.
     *
     * @since 0.1.0
     *
     * @return list<Message> Array of messages.
     */
    public function toMessages(): array
    {
        return array_values(array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->getMessage(), $this->candidates));
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'Unique identifier for this result.'], self::KEY_CANDIDATES => ['type' => 'array', 'items' => \WordPress\AiClient\Results\DTO\Candidate::getJsonSchema(), 'minItems' => 1, 'description' => 'The generated candidates.'], self::KEY_TOKEN_USAGE => \WordPress\AiClient\Results\DTO\TokenUsage::getJsonSchema(), self::KEY_PROVIDER_METADATA => ProviderMetadata::getJsonSchema(), self::KEY_MODEL_METADATA => ModelMetadata::getJsonSchema(), self::KEY_ADDITIONAL_DATA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Additional data included in the API response.']], 'required' => [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResultArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_CANDIDATES => array_map(fn(\WordPress\AiClient\Results\DTO\Candidate $candidate) => $candidate->toArray(), $this->candidates), self::KEY_TOKEN_USAGE => $this->tokenUsage->toArray(), self::KEY_PROVIDER_METADATA => $this->providerMetadata->toArray(), self::KEY_MODEL_METADATA => $this->modelMetadata->toArray(), self::KEY_ADDITIONAL_DATA => $this->additionalData];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_CANDIDATES, self::KEY_TOKEN_USAGE, self::KEY_PROVIDER_METADATA, self::KEY_MODEL_METADATA]);
        $candidates = array_map(fn(array $candidateData) => \WordPress\AiClient\Results\DTO\Candidate::fromArray($candidateData), $array[self::KEY_CANDIDATES]);
        return new self($array[self::KEY_ID], $candidates, \WordPress\AiClient\Results\DTO\TokenUsage::fromArray($array[self::KEY_TOKEN_USAGE]), ProviderMetadata::fromArray($array[self::KEY_PROVIDER_METADATA]), ModelMetadata::fromArray($array[self::KEY_MODEL_METADATA]), $array[self::KEY_ADDITIONAL_DATA] ?? []);
    }
    /**
     * Performs a deep clone of the result.
     *
     * This method ensures that all nested objects (candidates, token usage, metadata)
     * are cloned to prevent modifications to the cloned result from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedCandidates = [];
        foreach ($this->candidates as $candidate) {
            $clonedCandidates[] = clone $candidate;
        }
        $this->candidates = $clonedCandidates;
        $this->tokenUsage = clone $this->tokenUsage;
        $this->providerMetadata = clone $this->providerMetadata;
        $this->modelMetadata = clone $this->modelMetadata;
    }
}
                 Results/DTO/Candidate.php                                                                           0000644                 00000006647 15227644447 0011254 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Results\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
/**
 * Represents a candidate response from an AI model.
 *
 * When generating content, AI models can produce multiple candidates.
 * Each candidate contains a message and metadata about why generation stopped.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessageArrayShape from Message
 *
 * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string}
 *
 * @extends AbstractDataTransferObject<CandidateArrayShape>
 */
class Candidate extends AbstractDataTransferObject
{
    public const KEY_MESSAGE = 'message';
    public const KEY_FINISH_REASON = 'finishReason';
    /**
     * @var Message The generated message.
     */
    private Message $message;
    /**
     * @var FinishReasonEnum The reason generation stopped.
     */
    private FinishReasonEnum $finishReason;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param Message $message The generated message.
     * @param FinishReasonEnum $finishReason The reason generation stopped.
     */
    public function __construct(Message $message, FinishReasonEnum $finishReason)
    {
        if (!$message->getRole()->isModel()) {
            throw new InvalidArgumentException('Message must be a model message.');
        }
        $this->message = $message;
        $this->finishReason = $finishReason;
    }
    /**
     * Gets the generated message.
     *
     * @since 0.1.0
     *
     * @return Message The message.
     */
    public function getMessage(): Message
    {
        return $this->message;
    }
    /**
     * Gets the finish reason.
     *
     * @since 0.1.0
     *
     * @return FinishReasonEnum The finish reason.
     */
    public function getFinishReason(): FinishReasonEnum
    {
        return $this->finishReason;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_MESSAGE => Message::getJsonSchema(), self::KEY_FINISH_REASON => ['type' => 'string', 'enum' => FinishReasonEnum::getValues(), 'description' => 'The reason generation stopped.']], 'required' => [self::KEY_MESSAGE, self::KEY_FINISH_REASON]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return CandidateArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_MESSAGE => $this->message->toArray(), self::KEY_FINISH_REASON => $this->finishReason->value];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_MESSAGE, self::KEY_FINISH_REASON]);
        $messageData = $array[self::KEY_MESSAGE];
        return new self(Message::fromArray($messageData), FinishReasonEnum::from($array[self::KEY_FINISH_REASON]));
    }
    /**
     * Performs a deep clone of the candidate.
     *
     * This method ensures that the message object is cloned to prevent
     * modifications to the cloned candidate from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $this->message = clone $this->message;
    }
}
                                                                                         Providers/AbstractProvider.php                                                                      0000644                 00000010014 15227644447 0012523 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers;

use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for a provider.
 *
 * @since 0.1.0
 */
abstract class AbstractProvider implements ProviderInterface
{
    /**
     * @var array<string, ProviderMetadata> Cache for provider metadata per class.
     */
    private static array $metadataCache = [];
    /**
     * @var array<string, ProviderAvailabilityInterface> Cache for provider availability per class.
     */
    private static array $availabilityCache = [];
    /**
     * @var array<string, ModelMetadataDirectoryInterface> Cache for model metadata directory per class.
     */
    private static array $modelMetadataDirectoryCache = [];
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function metadata(): ProviderMetadata
    {
        $className = static::class;
        if (!isset(self::$metadataCache[$className])) {
            self::$metadataCache[$className] = static::createProviderMetadata();
        }
        return self::$metadataCache[$className];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
    {
        $providerMetadata = static::metadata();
        $modelMetadata = static::modelMetadataDirectory()->getModelMetadata($modelId);
        $model = static::createModel($modelMetadata, $providerMetadata);
        if ($modelConfig) {
            $model->setConfig($modelConfig);
        }
        return $model;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function availability(): ProviderAvailabilityInterface
    {
        $className = static::class;
        if (!isset(self::$availabilityCache[$className])) {
            self::$availabilityCache[$className] = static::createProviderAvailability();
        }
        return self::$availabilityCache[$className];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public static function modelMetadataDirectory(): ModelMetadataDirectoryInterface
    {
        $className = static::class;
        if (!isset(self::$modelMetadataDirectoryCache[$className])) {
            self::$modelMetadataDirectoryCache[$className] = static::createModelMetadataDirectory();
        }
        return self::$modelMetadataDirectoryCache[$className];
    }
    /**
     * Creates a model instance based on the given model metadata and provider metadata.
     *
     * @since 0.1.0
     *
     * @param ModelMetadata $modelMetadata The model metadata.
     * @param ProviderMetadata $providerMetadata The provider metadata.
     * @return ModelInterface The new model instance.
     */
    abstract protected static function createModel(ModelMetadata $modelMetadata, ProviderMetadata $providerMetadata): ModelInterface;
    /**
     * Creates the provider metadata instance.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    abstract protected static function createProviderMetadata(): ProviderMetadata;
    /**
     * Creates the provider availability instance.
     *
     * @since 0.1.0
     *
     * @return ProviderAvailabilityInterface The provider availability.
     */
    abstract protected static function createProviderAvailability(): ProviderAvailabilityInterface;
    /**
     * Creates the model metadata directory instance.
     *
     * @since 0.1.0
     *
     * @return ModelMetadataDirectoryInterface The model metadata directory.
     */
    abstract protected static function createModelMetadataDirectory(): ModelMetadataDirectoryInterface;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    Providers/Models/TextGeneration/Contracts/TextGenerationOperationModelInterface.php                 0000644                 00000001425 15227644447 0025021 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous text generation operations.
 *
 * Provides methods for initiating long-running text generation tasks.
 *
 * @since 0.1.0
 */
interface TextGenerationOperationModelInterface
{
    /**
     * Creates a text generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text generation prompt.
     * @return GenerativeAiOperation The initiated text generation operation.
     */
    public function generateTextOperation(array $prompt): GenerativeAiOperation;
}
                                                                                                                                                                                                                                           Providers/Models/TextGeneration/Contracts/TextGenerationModelInterface.php                          0000644                 00000001340 15227644447 0023134 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support text generation.
 *
 * Provides synchronous and streaming methods for generating text from prompts.
 *
 * @since 0.1.0
 */
interface TextGenerationModelInterface
{
    /**
     * Generates text from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text generation prompt.
     * @return GenerativeAiResult Result containing generated text.
     */
    public function generateTextResult(array $prompt): GenerativeAiResult;
}
                                                                                                                                                                                                                                                                                                Providers/Models/VideoGeneration/Contracts/VideoGenerationModelInterface.php                        0000644                 00000001335 15227644447 0023404 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\VideoGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support video generation.
 *
 * Provides synchronous methods for generating videos from prompts.
 *
 * @since 1.3.0
 */
interface VideoGenerationModelInterface
{
    /**
     * Generates videos from a prompt.
     *
     * @since 1.3.0
     *
     * @param list<Message> $prompt Array of messages containing the video generation prompt.
     * @return GenerativeAiResult Result containing generated videos.
     */
    public function generateVideoResult(array $prompt): GenerativeAiResult;
}
                                                                                                                                                                                                                                                                                                   Providers/Models/VideoGeneration/Contracts/VideoGenerationOperationModelInterface.php               0000644                 00000001435 15227644447 0025266 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\VideoGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous video generation operations.
 *
 * Provides methods for initiating long-running video generation tasks.
 *
 * @since 1.3.0
 */
interface VideoGenerationOperationModelInterface
{
    /**
     * Creates a video generation operation.
     *
     * @since 1.3.0
     *
     * @param list<Message> $prompt Array of messages containing the video generation prompt.
     * @return GenerativeAiOperation The initiated video generation operation.
     */
    public function generateVideoOperation(array $prompt): GenerativeAiOperation;
}
                                                                                                                                                                                                                                   Providers/Models/Contracts/ModelInterface.php                                                       0000644                 00000002357 15227644447 0015324 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Contracts;

use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Interface for AI models.
 *
 * Models represent specific AI models from providers and define
 * their capabilities, configuration, and execution methods.
 *
 * @since 0.1.0
 */
interface ModelInterface
{
    /**
     * Gets model metadata.
     *
     * @since 0.1.0
     *
     * @return ModelMetadata Model metadata.
     */
    public function metadata(): ModelMetadata;
    /**
     * Returns the metadata for the model's provider.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function providerMetadata(): ProviderMetadata;
    /**
     * Sets model configuration.
     *
     * @since 0.1.0
     *
     * @param ModelConfig $config Model configuration.
     * @return void
     */
    public function setConfig(ModelConfig $config): void;
    /**
     * Gets model configuration.
     *
     * @since 0.1.0
     *
     * @return ModelConfig Current model configuration.
     */
    public function getConfig(): ModelConfig;
}
                                                                                                                                                                                                                                                                                 Providers/Models/Enums/CapabilityEnum.php                                                           0000644                 00000005022 15227644447 0014470 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for model capabilities.
 *
 * @since 0.1.0
 *
 * @method static self textGeneration() Creates an instance for TEXT_GENERATION capability.
 * @method static self imageGeneration() Creates an instance for IMAGE_GENERATION capability.
 * @method static self textToSpeechConversion() Creates an instance for TEXT_TO_SPEECH_CONVERSION capability.
 * @method static self speechGeneration() Creates an instance for SPEECH_GENERATION capability.
 * @method static self musicGeneration() Creates an instance for MUSIC_GENERATION capability.
 * @method static self videoGeneration() Creates an instance for VIDEO_GENERATION capability.
 * @method static self embeddingGeneration() Creates an instance for EMBEDDING_GENERATION capability.
 * @method static self chatHistory() Creates an instance for CHAT_HISTORY capability.
 * @method bool isTextGeneration() Checks if the capability is TEXT_GENERATION.
 * @method bool isImageGeneration() Checks if the capability is IMAGE_GENERATION.
 * @method bool isTextToSpeechConversion() Checks if the capability is TEXT_TO_SPEECH_CONVERSION.
 * @method bool isSpeechGeneration() Checks if the capability is SPEECH_GENERATION.
 * @method bool isMusicGeneration() Checks if the capability is MUSIC_GENERATION.
 * @method bool isVideoGeneration() Checks if the capability is VIDEO_GENERATION.
 * @method bool isEmbeddingGeneration() Checks if the capability is EMBEDDING_GENERATION.
 * @method bool isChatHistory() Checks if the capability is CHAT_HISTORY.
 */
class CapabilityEnum extends AbstractEnum
{
    /**
     * Text generation capability.
     */
    public const TEXT_GENERATION = 'text_generation';
    /**
     * Image generation capability.
     */
    public const IMAGE_GENERATION = 'image_generation';
    /**
     * Text to speech conversion capability.
     */
    public const TEXT_TO_SPEECH_CONVERSION = 'text_to_speech_conversion';
    /**
     * Speech generation capability.
     */
    public const SPEECH_GENERATION = 'speech_generation';
    /**
     * Music generation capability.
     */
    public const MUSIC_GENERATION = 'music_generation';
    /**
     * Video generation capability.
     */
    public const VIDEO_GENERATION = 'video_generation';
    /**
     * Embedding generation capability.
     */
    public const EMBEDDING_GENERATION = 'embedding_generation';
    /**
     * Chat history support capability.
     */
    public const CHAT_HISTORY = 'chat_history';
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              Providers/Models/Enums/error_log                                                                    0000644                 00000002132 15227644447 0012765 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 16:16:50 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/CapabilityEnum.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/CapabilityEnum.php on line 29
[19-Jul-2026 11:58:18 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/CapabilityEnum.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/CapabilityEnum.php on line 29
[21-Jul-2026 05:09:48 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/OptionEnum.php:65
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/Enums/OptionEnum.php on line 65
                                                                                                                                                                                                                                                                                                                                                                                                                                      Providers/Models/Enums/OptionEnum.php                                                               0000644                 00000013451 15227644447 0013664 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\Enums;

use ReflectionClass;
use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
/**
 * Enum for model options.
 *
 * This enum dynamically includes all options from ModelConfig KEY_* constants
 * in addition to the explicitly defined constants below.
 *
 * Explicitly defined option (not in ModelConfig):
 * @method static self inputModalities() Creates an instance for INPUT_MODALITIES option.
 * @method bool isInputModalities() Checks if the option is INPUT_MODALITIES.
 *
 * Dynamically loaded from ModelConfig KEY_* constants:
 * @method static self candidateCount() Creates an instance for CANDIDATE_COUNT option.
 * @method static self customOptions() Creates an instance for CUSTOM_OPTIONS option.
 * @method static self frequencyPenalty() Creates an instance for FREQUENCY_PENALTY option.
 * @method static self functionDeclarations() Creates an instance for FUNCTION_DECLARATIONS option.
 * @method static self logprobs() Creates an instance for LOGPROBS option.
 * @method static self maxTokens() Creates an instance for MAX_TOKENS option.
 * @method static self outputFileType() Creates an instance for OUTPUT_FILE_TYPE option.
 * @method static self outputMediaAspectRatio() Creates an instance for OUTPUT_MEDIA_ASPECT_RATIO option.
 * @method static self outputMediaOrientation() Creates an instance for OUTPUT_MEDIA_ORIENTATION option.
 * @method static self outputMimeType() Creates an instance for OUTPUT_MIME_TYPE option.
 * @method static self outputModalities() Creates an instance for OUTPUT_MODALITIES option.
 * @method static self outputSchema() Creates an instance for OUTPUT_SCHEMA option.
 * @method static self outputSpeechVoice() Creates an instance for OUTPUT_SPEECH_VOICE option.
 * @method static self presencePenalty() Creates an instance for PRESENCE_PENALTY option.
 * @method static self stopSequences() Creates an instance for STOP_SEQUENCES option.
 * @method static self systemInstruction() Creates an instance for SYSTEM_INSTRUCTION option.
 * @method static self temperature() Creates an instance for TEMPERATURE option.
 * @method static self topK() Creates an instance for TOP_K option.
 * @method static self topLogprobs() Creates an instance for TOP_LOGPROBS option.
 * @method static self topP() Creates an instance for TOP_P option.
 * @method static self webSearch() Creates an instance for WEB_SEARCH option.
 * @method bool isCandidateCount() Checks if the option is CANDIDATE_COUNT.
 * @method bool isCustomOptions() Checks if the option is CUSTOM_OPTIONS.
 * @method bool isFrequencyPenalty() Checks if the option is FREQUENCY_PENALTY.
 * @method bool isFunctionDeclarations() Checks if the option is FUNCTION_DECLARATIONS.
 * @method bool isLogprobs() Checks if the option is LOGPROBS.
 * @method bool isMaxTokens() Checks if the option is MAX_TOKENS.
 * @method bool isOutputFileType() Checks if the option is OUTPUT_FILE_TYPE.
 * @method bool isOutputMediaAspectRatio() Checks if the option is OUTPUT_MEDIA_ASPECT_RATIO.
 * @method bool isOutputMediaOrientation() Checks if the option is OUTPUT_MEDIA_ORIENTATION.
 * @method bool isOutputMimeType() Checks if the option is OUTPUT_MIME_TYPE.
 * @method bool isOutputModalities() Checks if the option is OUTPUT_MODALITIES.
 * @method bool isOutputSchema() Checks if the option is OUTPUT_SCHEMA.
 * @method bool isOutputSpeechVoice() Checks if the option is OUTPUT_SPEECH_VOICE.
 * @method bool isPresencePenalty() Checks if the option is PRESENCE_PENALTY.
 * @method bool isStopSequences() Checks if the option is STOP_SEQUENCES.
 * @method bool isSystemInstruction() Checks if the option is SYSTEM_INSTRUCTION.
 * @method bool isTemperature() Checks if the option is TEMPERATURE.
 * @method bool isTopK() Checks if the option is TOP_K.
 * @method bool isTopLogprobs() Checks if the option is TOP_LOGPROBS.
 * @method bool isTopP() Checks if the option is TOP_P.
 * @method bool isWebSearch() Checks if the option is WEB_SEARCH.
 *
 * @since 0.1.0
 */
class OptionEnum extends AbstractEnum
{
    /**
     * Input modalities option.
     *
     * This constant is not in ModelConfig as it's derived from message content,
     * not configured directly.
     */
    public const INPUT_MODALITIES = 'input_modalities';
    /**
     * Determines the class enumerations by reflecting on class constants.
     *
     * Overrides the parent method to dynamically add constants from ModelConfig
     * that are prefixed with KEY_. These are transformed to remove the KEY_ prefix
     * and converted to snake_case values.
     *
     * @since 0.1.0
     *
     * @param class-string $className The fully qualified class name.
     * @return array<string, string> The enum constants.
     */
    protected static function determineClassEnumerations(string $className): array
    {
        // Start with the constants defined in this class using parent method
        $constants = parent::determineClassEnumerations($className);
        // Use reflection to get all constants from ModelConfig
        $modelConfigReflection = new ReflectionClass(ModelConfig::class);
        $modelConfigConstants = $modelConfigReflection->getConstants();
        // Add ModelConfig constants that start with KEY_
        foreach ($modelConfigConstants as $constantName => $constantValue) {
            if (str_starts_with($constantName, 'KEY_')) {
                // Remove KEY_ prefix to get the enum constant name
                $enumConstantName = substr($constantName, 4);
                // The value is the snake_case version stored in ModelConfig
                // ModelConfig already stores these as snake_case strings
                if (is_string($constantValue)) {
                    $constants[$enumConstantName] = $constantValue;
                }
            }
        }
        return $constants;
    }
}
                                                                                                                                                                                                                       Providers/Models/SpeechGeneration/Contracts/SpeechGenerationOperationModelInterface.php             0000644                 00000001445 15227644447 0025571 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous speech generation operations.
 *
 * Provides methods for initiating long-running speech generation tasks.
 *
 * @since 0.1.0
 */
interface SpeechGenerationOperationModelInterface
{
    /**
     * Creates a speech generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the speech generation prompt.
     * @return GenerativeAiOperation The initiated speech generation operation.
     */
    public function generateSpeechOperation(array $prompt): GenerativeAiOperation;
}
                                                                                                                                                                                                                           Providers/Models/SpeechGeneration/Contracts/SpeechGenerationModelInterface.php                      0000644                 00000001350 15227644447 0023703 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support speech generation.
 *
 * Provides synchronous methods for generating speech from prompts.
 *
 * @since 0.1.0
 */
interface SpeechGenerationModelInterface
{
    /**
     * Generates speech from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the speech generation prompt.
     * @return GenerativeAiResult Result containing generated speech audio.
     */
    public function generateSpeechResult(array $prompt): GenerativeAiResult;
}
                                                                                                                                                                                                                                                                                        Providers/Models/DTO/RequiredOption.php                                                             0000644                 00000005513 15227644447 0014077 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents an option that the implementing code requires the model to support.
 *
 * This class defines an option that the model must support with a specific value
 * for it to be considered suitable for the implementing code's requirements.
 *
 * @since 0.1.0
 *
 * @phpstan-type RequiredOptionArrayShape array{
 *     name: string,
 *     value: mixed
 * }
 *
 * @extends AbstractDataTransferObject<RequiredOptionArrayShape>
 */
class RequiredOption extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_VALUE = 'value';
    /**
     * @var OptionEnum The option name.
     */
    protected OptionEnum $name;
    /**
     * @var mixed The value that the model must support for this option.
     */
    protected $value;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param OptionEnum $name The option name.
     * @param mixed $value The value that the model must support for this option.
     */
    public function __construct(OptionEnum $name, $value)
    {
        $this->name = $name;
        $this->value = $value;
    }
    /**
     * Gets the option name.
     *
     * @since 0.1.0
     *
     * @return OptionEnum The option name.
     */
    public function getName(): OptionEnum
    {
        return $this->name;
    }
    /**
     * Gets the value that the model must support for this option.
     *
     * @since 0.1.0
     *
     * @return mixed The value that the model must support.
     */
    public function getValue()
    {
        return $this->value;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_VALUE => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']], 'description' => 'The value that the model must support for this option.']], 'required' => [self::KEY_NAME, self::KEY_VALUE]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return RequiredOptionArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_NAME => $this->name->value, self::KEY_VALUE => $this->value];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME, self::KEY_VALUE]);
        return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_VALUE]);
    }
}
                                                                                                                                                                                     Providers/Models/DTO/SupportedOption.php                                                            0000644                 00000014011 15227644447 0014275 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents a supported configuration option for an AI model.
 *
 * This class defines an option that a model supports, including its name
 * and the values that are valid for that option.
 *
 * @since 0.1.0
 *
 * @phpstan-type SupportedOptionArrayShape array{
 *     name: string,
 *     supportedValues?: list<mixed>
 * }
 *
 * @extends AbstractDataTransferObject<SupportedOptionArrayShape>
 */
class SupportedOption extends AbstractDataTransferObject
{
    public const KEY_NAME = 'name';
    public const KEY_SUPPORTED_VALUES = 'supportedValues';
    /**
     * @var OptionEnum The option name.
     */
    protected OptionEnum $name;
    /**
     * @var list<mixed>|null The supported values for this option.
     */
    protected ?array $supportedValues;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param OptionEnum $name The option name.
     * @param list<mixed>|null $supportedValues The supported values for this option, or null if any value is supported.
     *
     * @throws InvalidArgumentException If supportedValues is not null and not a list.
     */
    public function __construct(OptionEnum $name, ?array $supportedValues = null)
    {
        if ($supportedValues !== null && !array_is_list($supportedValues)) {
            throw new InvalidArgumentException('Supported values must be a list array.');
        }
        $this->name = $name;
        $this->supportedValues = $supportedValues;
    }
    /**
     * Gets the option name.
     *
     * @since 0.1.0
     *
     * @return OptionEnum The option name.
     */
    public function getName(): OptionEnum
    {
        return $this->name;
    }
    /**
     * Checks if a value is supported for this option.
     *
     * @since 0.1.0
     *
     * @param mixed $value The value to check.
     * @return bool True if the value is supported, false otherwise.
     */
    public function isSupportedValue($value): bool
    {
        // If supportedValues is null, any value is supported
        if ($this->supportedValues === null) {
            return \true;
        }
        // If the value is an array, consider it a set (i.e. order doesn't matter).
        if (is_array($value)) {
            $normalizedValue = self::normalizeArrayForComparison($value);
            foreach ($this->supportedValues as $supportedValue) {
                if (!is_array($supportedValue)) {
                    continue;
                }
                $normalizedSupported = self::normalizeArrayForComparison($supportedValue);
                if ($normalizedValue === $normalizedSupported) {
                    return \true;
                }
            }
            return \false;
        }
        $normalizedValue = self::normalizeValue($value);
        foreach ($this->supportedValues as $supportedValue) {
            if (self::normalizeValue($supportedValue) === $normalizedValue) {
                return \true;
            }
        }
        return \false;
    }
    /**
     * Normalizes an AbstractEnum instance to its string value.
     *
     * This ensures comparisons work correctly even after deserialization
     * (e.g. Redis/Memcached object cache), where AbstractEnum singletons
     * are reconstructed as separate instances.
     *
     * @since 1.2.1
     *
     * @param mixed $value The value to normalize.
     * @return mixed The normalized value.
     */
    private static function normalizeValue($value)
    {
        if ($value instanceof AbstractEnum) {
            return $value->value;
        }
        return $value;
    }
    /**
     * Normalizes and sorts an array for comparison.
     *
     * Maps each element through normalizeValue() and sorts the result,
     * ensuring consistent comparison regardless of element order or
     * AbstractEnum instance identity.
     *
     * @since 1.2.1
     *
     * @param array<mixed> $items The array to normalize.
     * @return array<mixed> The normalized, sorted array.
     */
    private static function normalizeArrayForComparison(array $items): array
    {
        $normalized = array_map([self::class, 'normalizeValue'], $items);
        sort($normalized);
        return $normalized;
    }
    /**
     * Gets the supported values for this option.
     *
     * @since 0.1.0
     *
     * @return list<mixed>|null The supported values, or null if any value is supported.
     */
    public function getSupportedValues(): ?array
    {
        return $this->supportedValues;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_NAME => ['type' => 'string', 'enum' => OptionEnum::getValues(), 'description' => 'The option name.'], self::KEY_SUPPORTED_VALUES => ['type' => 'array', 'items' => ['oneOf' => [['type' => 'string'], ['type' => 'number'], ['type' => 'boolean'], ['type' => 'null'], ['type' => 'array'], ['type' => 'object']]], 'description' => 'The supported values for this option.']], 'required' => [self::KEY_NAME]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return SupportedOptionArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_NAME => $this->name->value];
        if ($this->supportedValues !== null) {
            /** @var list<mixed> $supportedValues */
            $supportedValues = $this->supportedValues;
            $data[self::KEY_SUPPORTED_VALUES] = $supportedValues;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_NAME]);
        return new self(OptionEnum::from($array[self::KEY_NAME]), $array[self::KEY_SUPPORTED_VALUES] ?? null);
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       Providers/Models/DTO/error_log                                                                      0000644                 00000007400 15227644447 0012327 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 03:31:48 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/SupportedOption.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/SupportedOption.php on line 25
[19-Jul-2026 03:31:48 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelRequirements.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelRequirements.php on line 29
[19-Jul-2026 03:31:48 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/RequiredOption.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/RequiredOption.php on line 23
[19-Jul-2026 03:31:48 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelMetadata.php:28
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelMetadata.php on line 28
[19-Jul-2026 03:31:49 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelConfig.php:51
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelConfig.php on line 51
[20-Jul-2026 06:58:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/SupportedOption.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/SupportedOption.php on line 25
[20-Jul-2026 06:58:01 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelRequirements.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelRequirements.php on line 29
[20-Jul-2026 06:58:01 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/RequiredOption.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/RequiredOption.php on line 23
[20-Jul-2026 06:58:01 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelMetadata.php:28
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelMetadata.php on line 28
[20-Jul-2026 06:58:01 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelConfig.php:51
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelConfig.php on line 51
                                                                                                                                                                                                                                                                Providers/Models/DTO/ModelMetadata.php                                                              0000644                 00000013770 15227644447 0013633 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
/**
 * Represents metadata about an AI model.
 *
 * This class contains information about a specific AI model, including
 * its identifier, display name, supported capabilities, and configuration options.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type SupportedOptionArrayShape from SupportedOption
 *
 * @phpstan-type ModelMetadataArrayShape array{
 *     id: string,
 *     name: string,
 *     supportedCapabilities: list<string>,
 *     supportedOptions: list<SupportedOptionArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ModelMetadataArrayShape>
 */
class ModelMetadata extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_SUPPORTED_CAPABILITIES = 'supportedCapabilities';
    public const KEY_SUPPORTED_OPTIONS = 'supportedOptions';
    /**
     * @var string The model's unique identifier.
     */
    protected string $id;
    /**
     * @var string The model's display name.
     */
    protected string $name;
    /**
     * @var list<CapabilityEnum> The model's supported capabilities.
     */
    protected array $supportedCapabilities;
    /**
     * @var list<SupportedOption> The model's supported configuration options.
     */
    protected array $supportedOptions;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $id The model's unique identifier.
     * @param string $name The model's display name.
     * @param list<CapabilityEnum> $supportedCapabilities The model's supported capabilities.
     * @param list<SupportedOption> $supportedOptions The model's supported configuration options.
     *
     * @throws InvalidArgumentException If arrays are not lists.
     */
    public function __construct(string $id, string $name, array $supportedCapabilities, array $supportedOptions)
    {
        if (!array_is_list($supportedCapabilities)) {
            throw new InvalidArgumentException('Supported capabilities must be a list array.');
        }
        if (!array_is_list($supportedOptions)) {
            throw new InvalidArgumentException('Supported options must be a list array.');
        }
        $this->id = $id;
        $this->name = $name;
        $this->supportedCapabilities = $supportedCapabilities;
        $this->supportedOptions = $supportedOptions;
    }
    /**
     * Gets the model's unique identifier.
     *
     * @since 0.1.0
     *
     * @return string The model ID.
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the model's display name.
     *
     * @since 0.1.0
     *
     * @return string The model name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the model's supported capabilities.
     *
     * @since 0.1.0
     *
     * @return list<CapabilityEnum> The supported capabilities.
     */
    public function getSupportedCapabilities(): array
    {
        return $this->supportedCapabilities;
    }
    /**
     * Gets the model's supported configuration options.
     *
     * @since 0.1.0
     *
     * @return list<SupportedOption> The supported options.
     */
    public function getSupportedOptions(): array
    {
        return $this->supportedOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The model\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The model\'s display name.'], self::KEY_SUPPORTED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The model\'s supported capabilities.'], self::KEY_SUPPORTED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::getJsonSchema(), 'description' => 'The model\'s supported configuration options.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_SUPPORTED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->supportedCapabilities), self::KEY_SUPPORTED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\SupportedOption $option): array => $option->toArray(), $this->supportedOptions)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_SUPPORTED_CAPABILITIES, self::KEY_SUPPORTED_OPTIONS]);
        return new self($array[self::KEY_ID], $array[self::KEY_NAME], array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_SUPPORTED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\SupportedOption => \WordPress\AiClient\Providers\Models\DTO\SupportedOption::fromArray($optionData), $array[self::KEY_SUPPORTED_OPTIONS]));
    }
    /**
     * Performs a deep clone of the model metadata.
     *
     * This method ensures that supported option objects are cloned to prevent
     * modifications to the cloned metadata from affecting the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        $clonedOptions = [];
        foreach ($this->supportedOptions as $option) {
            $clonedOptions[] = clone $option;
        }
        $this->supportedOptions = $clonedOptions;
    }
}
        Providers/Models/DTO/ModelConfig.php                                                                0000644                 00000073244 15227644447 0013322 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
use WordPress\AiClient\Tools\DTO\WebSearch;
/**
 * Represents configuration for an AI model.
 *
 * This class allows configuring various parameters for model behavior,
 * including output modalities, system instructions, generation parameters,
 * and tool integrations.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type FunctionDeclarationArrayShape from FunctionDeclaration
 * @phpstan-import-type WebSearchArrayShape from WebSearch
 *
 * @phpstan-type ModelConfigArrayShape array{
 *     outputModalities?: list<string>,
 *     systemInstruction?: string,
 *     candidateCount?: int,
 *     maxTokens?: int,
 *     temperature?: float,
 *     topP?: float,
 *     topK?: int,
 *     stopSequences?: list<string>,
 *     presencePenalty?: float,
 *     frequencyPenalty?: float,
 *     logprobs?: bool,
 *     topLogprobs?: int,
 *     functionDeclarations?: list<FunctionDeclarationArrayShape>,
 *     webSearch?: WebSearchArrayShape,
 *     outputFileType?: string,
 *     outputMimeType?: string,
 *     outputSchema?: array<string, mixed>,
 *     outputMediaOrientation?: string,
 *     outputMediaAspectRatio?: string,
 *     outputSpeechVoice?: string,
 *     customOptions?: array<string, mixed>
 * }
 *
 * @extends AbstractDataTransferObject<ModelConfigArrayShape>
 */
class ModelConfig extends AbstractDataTransferObject
{
    public const KEY_OUTPUT_MODALITIES = 'outputModalities';
    public const KEY_SYSTEM_INSTRUCTION = 'systemInstruction';
    public const KEY_CANDIDATE_COUNT = 'candidateCount';
    public const KEY_MAX_TOKENS = 'maxTokens';
    public const KEY_TEMPERATURE = 'temperature';
    public const KEY_TOP_P = 'topP';
    public const KEY_TOP_K = 'topK';
    public const KEY_STOP_SEQUENCES = 'stopSequences';
    public const KEY_PRESENCE_PENALTY = 'presencePenalty';
    public const KEY_FREQUENCY_PENALTY = 'frequencyPenalty';
    public const KEY_LOGPROBS = 'logprobs';
    public const KEY_TOP_LOGPROBS = 'topLogprobs';
    public const KEY_FUNCTION_DECLARATIONS = 'functionDeclarations';
    public const KEY_WEB_SEARCH = 'webSearch';
    public const KEY_OUTPUT_FILE_TYPE = 'outputFileType';
    public const KEY_OUTPUT_MIME_TYPE = 'outputMimeType';
    public const KEY_OUTPUT_SCHEMA = 'outputSchema';
    public const KEY_OUTPUT_MEDIA_ORIENTATION = 'outputMediaOrientation';
    public const KEY_OUTPUT_MEDIA_ASPECT_RATIO = 'outputMediaAspectRatio';
    public const KEY_OUTPUT_SPEECH_VOICE = 'outputSpeechVoice';
    public const KEY_CUSTOM_OPTIONS = 'customOptions';
    /*
     * Note: This key is not an actual model config key, but specified here for convenience.
     * It is relevant for model discovery, to determine which models support which input modalities.
     * The actual input modalities are part of the message sent to the model, not the model config.
     */
    public const KEY_INPUT_MODALITIES = 'inputModalities';
    /**
     * @var list<ModalityEnum>|null Output modalities for the model.
     */
    protected ?array $outputModalities = null;
    /**
     * @var string|null System instruction for the model.
     */
    protected ?string $systemInstruction = null;
    /**
     * @var int|null Number of response candidates to generate.
     */
    protected ?int $candidateCount = null;
    /**
     * @var int|null Maximum number of tokens to generate.
     */
    protected ?int $maxTokens = null;
    /**
     * @var float|null Temperature for randomness (0.0 to 2.0).
     */
    protected ?float $temperature = null;
    /**
     * @var float|null Top-p nucleus sampling parameter.
     */
    protected ?float $topP = null;
    /**
     * @var int|null Top-k sampling parameter.
     */
    protected ?int $topK = null;
    /**
     * @var list<string>|null Stop sequences.
     */
    protected ?array $stopSequences = null;
    /**
     * @var float|null Presence penalty for reducing repetition.
     */
    protected ?float $presencePenalty = null;
    /**
     * @var float|null Frequency penalty for reducing repetition.
     */
    protected ?float $frequencyPenalty = null;
    /**
     * @var bool|null Whether to return log probabilities.
     */
    protected ?bool $logprobs = null;
    /**
     * @var int|null Number of top log probabilities to return.
     */
    protected ?int $topLogprobs = null;
    /**
     * @var list<FunctionDeclaration>|null Function declarations available to the model.
     */
    protected ?array $functionDeclarations = null;
    /**
     * @var WebSearch|null Web search configuration for the model.
     */
    protected ?WebSearch $webSearch = null;
    /**
     * @var FileTypeEnum|null Output file type.
     */
    protected ?FileTypeEnum $outputFileType = null;
    /**
     * @var string|null Output MIME type.
     */
    protected ?string $outputMimeType = null;
    /**
     * @var array<string, mixed>|null Output schema (JSON schema).
     */
    protected ?array $outputSchema = null;
    /**
     * @var MediaOrientationEnum|null Output media orientation.
     */
    protected ?MediaOrientationEnum $outputMediaOrientation = null;
    /**
     * @var string|null Output media aspect ratio (e.g. 3:2, 16:9).
     */
    protected ?string $outputMediaAspectRatio = null;
    /**
     * @var string|null Output speech voice.
     */
    protected ?string $outputSpeechVoice = null;
    /**
     * @var array<string, mixed> Custom provider-specific options.
     */
    protected array $customOptions = [];
    /**
     * Creates a deep clone of this configuration.
     *
     * Clones nested objects (functionDeclarations, webSearch) to ensure
     * the cloned configuration is independent of the original.
     * Enum value objects (outputModalities, outputFileType, outputMediaOrientation)
     * are intentionally shared as they are immutable.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone function declarations if set
        if ($this->functionDeclarations !== null) {
            $clonedDeclarations = [];
            foreach ($this->functionDeclarations as $declaration) {
                $clonedDeclarations[] = clone $declaration;
            }
            $this->functionDeclarations = $clonedDeclarations;
        }
        // Clone web search if set
        if ($this->webSearch !== null) {
            $this->webSearch = clone $this->webSearch;
        }
        // Note: Enum value objects (outputModalities, outputFileType, outputMediaOrientation)
        // are immutable and can be safely shared.
    }
    /**
     * Sets the output modalities.
     *
     * @since 0.1.0
     *
     * @param list<ModalityEnum> $outputModalities The output modalities.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setOutputModalities(array $outputModalities): void
    {
        if (!array_is_list($outputModalities)) {
            throw new InvalidArgumentException('Output modalities must be a list array.');
        }
        $this->outputModalities = $outputModalities;
    }
    /**
     * Gets the output modalities.
     *
     * @since 0.1.0
     *
     * @return list<ModalityEnum>|null The output modalities.
     */
    public function getOutputModalities(): ?array
    {
        return $this->outputModalities;
    }
    /**
     * Sets the system instruction.
     *
     * @since 0.1.0
     *
     * @param string $systemInstruction The system instruction.
     */
    public function setSystemInstruction(string $systemInstruction): void
    {
        $this->systemInstruction = $systemInstruction;
    }
    /**
     * Gets the system instruction.
     *
     * @since 0.1.0
     *
     * @return string|null The system instruction.
     */
    public function getSystemInstruction(): ?string
    {
        return $this->systemInstruction;
    }
    /**
     * Sets the candidate count.
     *
     * @since 0.1.0
     *
     * @param int $candidateCount The candidate count.
     */
    public function setCandidateCount(int $candidateCount): void
    {
        $this->candidateCount = $candidateCount;
    }
    /**
     * Gets the candidate count.
     *
     * @since 0.1.0
     *
     * @return int|null The candidate count.
     */
    public function getCandidateCount(): ?int
    {
        return $this->candidateCount;
    }
    /**
     * Sets the maximum tokens.
     *
     * @since 0.1.0
     *
     * @param int $maxTokens The maximum tokens.
     */
    public function setMaxTokens(int $maxTokens): void
    {
        $this->maxTokens = $maxTokens;
    }
    /**
     * Gets the maximum tokens.
     *
     * @since 0.1.0
     *
     * @return int|null The maximum tokens.
     */
    public function getMaxTokens(): ?int
    {
        return $this->maxTokens;
    }
    /**
     * Sets the temperature.
     *
     * @since 0.1.0
     *
     * @param float $temperature The temperature.
     */
    public function setTemperature(float $temperature): void
    {
        $this->temperature = $temperature;
    }
    /**
     * Gets the temperature.
     *
     * @since 0.1.0
     *
     * @return float|null The temperature.
     */
    public function getTemperature(): ?float
    {
        return $this->temperature;
    }
    /**
     * Sets the top-p parameter.
     *
     * @since 0.1.0
     *
     * @param float $topP The top-p parameter.
     */
    public function setTopP(float $topP): void
    {
        $this->topP = $topP;
    }
    /**
     * Gets the top-p parameter.
     *
     * @since 0.1.0
     *
     * @return float|null The top-p parameter.
     */
    public function getTopP(): ?float
    {
        return $this->topP;
    }
    /**
     * Sets the top-k parameter.
     *
     * @since 0.1.0
     *
     * @param int $topK The top-k parameter.
     */
    public function setTopK(int $topK): void
    {
        $this->topK = $topK;
    }
    /**
     * Gets the top-k parameter.
     *
     * @since 0.1.0
     *
     * @return int|null The top-k parameter.
     */
    public function getTopK(): ?int
    {
        return $this->topK;
    }
    /**
     * Sets the stop sequences.
     *
     * @since 0.1.0
     *
     * @param list<string> $stopSequences The stop sequences.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setStopSequences(array $stopSequences): void
    {
        if (!array_is_list($stopSequences)) {
            throw new InvalidArgumentException('Stop sequences must be a list array.');
        }
        $this->stopSequences = $stopSequences;
    }
    /**
     * Gets the stop sequences.
     *
     * @since 0.1.0
     *
     * @return list<string>|null The stop sequences.
     */
    public function getStopSequences(): ?array
    {
        return $this->stopSequences;
    }
    /**
     * Sets the presence penalty.
     *
     * @since 0.1.0
     *
     * @param float $presencePenalty The presence penalty.
     */
    public function setPresencePenalty(float $presencePenalty): void
    {
        $this->presencePenalty = $presencePenalty;
    }
    /**
     * Gets the presence penalty.
     *
     * @since 0.1.0
     *
     * @return float|null The presence penalty.
     */
    public function getPresencePenalty(): ?float
    {
        return $this->presencePenalty;
    }
    /**
     * Sets the frequency penalty.
     *
     * @since 0.1.0
     *
     * @param float $frequencyPenalty The frequency penalty.
     */
    public function setFrequencyPenalty(float $frequencyPenalty): void
    {
        $this->frequencyPenalty = $frequencyPenalty;
    }
    /**
     * Gets the frequency penalty.
     *
     * @since 0.1.0
     *
     * @return float|null The frequency penalty.
     */
    public function getFrequencyPenalty(): ?float
    {
        return $this->frequencyPenalty;
    }
    /**
     * Sets whether to return log probabilities.
     *
     * @since 0.1.0
     *
     * @param bool $logprobs Whether to return log probabilities.
     */
    public function setLogprobs(bool $logprobs): void
    {
        $this->logprobs = $logprobs;
    }
    /**
     * Gets whether to return log probabilities.
     *
     * @since 0.1.0
     *
     * @return bool|null Whether to return log probabilities.
     */
    public function getLogprobs(): ?bool
    {
        return $this->logprobs;
    }
    /**
     * Sets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @param int $topLogprobs The number of top log probabilities.
     */
    public function setTopLogprobs(int $topLogprobs): void
    {
        $this->topLogprobs = $topLogprobs;
    }
    /**
     * Gets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @return int|null The number of top log probabilities.
     */
    public function getTopLogprobs(): ?int
    {
        return $this->topLogprobs;
    }
    /**
     * Sets the function declarations.
     *
     * @since 0.1.0
     *
     * @param list<FunctionDeclaration> $functionDeclarations The function declarations.
     *
     * @throws InvalidArgumentException If the array is not a list.
     */
    public function setFunctionDeclarations(array $functionDeclarations): void
    {
        if (!array_is_list($functionDeclarations)) {
            throw new InvalidArgumentException('Function declarations must be a list array.');
        }
        $this->functionDeclarations = $functionDeclarations;
    }
    /**
     * Gets the function declarations.
     *
     * @since 0.1.0
     *
     * @return list<FunctionDeclaration>|null The function declarations.
     */
    public function getFunctionDeclarations(): ?array
    {
        return $this->functionDeclarations;
    }
    /**
     * Sets the web search configuration.
     *
     * @since 0.1.0
     *
     * @param WebSearch $webSearch The web search configuration.
     */
    public function setWebSearch(WebSearch $webSearch): void
    {
        $this->webSearch = $webSearch;
    }
    /**
     * Gets the web search configuration.
     *
     * @since 0.1.0
     *
     * @return WebSearch|null The web search configuration.
     */
    public function getWebSearch(): ?WebSearch
    {
        return $this->webSearch;
    }
    /**
     * Sets the output file type.
     *
     * @since 0.1.0
     *
     * @param FileTypeEnum $outputFileType The output file type.
     */
    public function setOutputFileType(FileTypeEnum $outputFileType): void
    {
        $this->outputFileType = $outputFileType;
    }
    /**
     * Gets the output file type.
     *
     * @since 0.1.0
     *
     * @return FileTypeEnum|null The output file type.
     */
    public function getOutputFileType(): ?FileTypeEnum
    {
        return $this->outputFileType;
    }
    /**
     * Sets the output MIME type.
     *
     * @since 0.1.0
     *
     * @param string $outputMimeType The output MIME type.
     */
    public function setOutputMimeType(string $outputMimeType): void
    {
        $this->outputMimeType = $outputMimeType;
    }
    /**
     * Gets the output MIME type.
     *
     * @since 0.1.0
     *
     * @return string|null The output MIME type.
     */
    public function getOutputMimeType(): ?string
    {
        return $this->outputMimeType;
    }
    /**
     * Sets the output schema.
     *
     * When setting an output schema, this method automatically sets
     * the output MIME type to "application/json" if not already set.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $outputSchema The output schema (JSON schema).
     */
    public function setOutputSchema(array $outputSchema): void
    {
        $this->outputSchema = $outputSchema;
        // Automatically set outputMimeType to application/json when schema is provided
        if ($this->outputMimeType === null) {
            $this->outputMimeType = 'application/json';
        }
    }
    /**
     * Gets the output schema.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The output schema.
     */
    public function getOutputSchema(): ?array
    {
        return $this->outputSchema;
    }
    /**
     * Sets the output media orientation.
     *
     * @since 0.1.0
     *
     * @param MediaOrientationEnum $outputMediaOrientation The output media orientation.
     */
    public function setOutputMediaOrientation(MediaOrientationEnum $outputMediaOrientation): void
    {
        if ($this->outputMediaAspectRatio) {
            $this->validateMediaOrientationAspectRatioCompatibility($outputMediaOrientation, $this->outputMediaAspectRatio);
        }
        $this->outputMediaOrientation = $outputMediaOrientation;
    }
    /**
     * Gets the output media orientation.
     *
     * @since 0.1.0
     *
     * @return MediaOrientationEnum|null The output media orientation.
     */
    public function getOutputMediaOrientation(): ?MediaOrientationEnum
    {
        return $this->outputMediaOrientation;
    }
    /**
     * Sets the output media aspect ratio.
     *
     * If set, this supersedes the output media orientation, as it is a more specific configuration.
     *
     * @since 0.1.0
     *
     * @param string $outputMediaAspectRatio The output media aspect ratio (e.g. 3:2, 16:9).
     */
    public function setOutputMediaAspectRatio(string $outputMediaAspectRatio): void
    {
        if (!preg_match('/^\d+:\d+$/', $outputMediaAspectRatio)) {
            throw new InvalidArgumentException('Output media aspect ratio must be in the format "width:height" (e.g. 3:2, 16:9).');
        }
        if ($this->outputMediaOrientation) {
            $this->validateMediaOrientationAspectRatioCompatibility($this->outputMediaOrientation, $outputMediaAspectRatio);
        }
        $this->outputMediaAspectRatio = $outputMediaAspectRatio;
    }
    /**
     * Gets the output media aspect ratio.
     *
     * @since 0.1.0
     *
     * @return string|null The output media aspect ratio (e.g. 3:2, 16:9).
     */
    public function getOutputMediaAspectRatio(): ?string
    {
        return $this->outputMediaAspectRatio;
    }
    /**
     * Validates that the given media orientation and aspect ratio values do not conflict with each other.
     *
     * @since 0.4.0
     *
     * @param MediaOrientationEnum $orientation The desired media orientation.
     * @param string $aspectRatio The desired media aspect ratio.
     */
    protected function validateMediaOrientationAspectRatioCompatibility(MediaOrientationEnum $orientation, string $aspectRatio): void
    {
        $aspectRatioParts = explode(':', $aspectRatio);
        if ($orientation->isSquare() && $aspectRatioParts[0] !== $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the square orientation.');
        }
        if ($orientation->isLandscape() && $aspectRatioParts[0] <= $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the landscape orientation.');
        }
        if ($orientation->isPortrait() && $aspectRatioParts[0] >= $aspectRatioParts[1]) {
            throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not compatible with the portrait orientation.');
        }
    }
    /**
     * Sets the output speech voice.
     *
     * @since 0.1.0
     *
     * @param string $outputSpeechVoice The output speech voice.
     */
    public function setOutputSpeechVoice(string $outputSpeechVoice): void
    {
        $this->outputSpeechVoice = $outputSpeechVoice;
    }
    /**
     * Gets the output speech voice.
     *
     * @since 0.1.0
     *
     * @return string|null The output speech voice.
     */
    public function getOutputSpeechVoice(): ?string
    {
        return $this->outputSpeechVoice;
    }
    /**
     * Sets a single custom option.
     *
     * @since 0.1.0
     *
     * @param string $key   The option key.
     * @param mixed  $value The option value.
     */
    public function setCustomOption(string $key, $value): void
    {
        $this->customOptions[$key] = $value;
    }
    /**
     * Sets the custom options.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $customOptions The custom options.
     */
    public function setCustomOptions(array $customOptions): void
    {
        $this->customOptions = $customOptions;
    }
    /**
     * Gets the custom options.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed> The custom options.
     */
    public function getCustomOptions(): array
    {
        return $this->customOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_OUTPUT_MODALITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => ModalityEnum::getValues()], 'description' => 'Output modalities for the model.'], self::KEY_SYSTEM_INSTRUCTION => ['type' => 'string', 'description' => 'System instruction for the model.'], self::KEY_CANDIDATE_COUNT => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of response candidates to generate.'], self::KEY_MAX_TOKENS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Maximum number of tokens to generate.'], self::KEY_TEMPERATURE => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 2.0, 'description' => 'Temperature for randomness.'], self::KEY_TOP_P => ['type' => 'number', 'minimum' => 0.0, 'maximum' => 1.0, 'description' => 'Top-p nucleus sampling parameter.'], self::KEY_TOP_K => ['type' => 'integer', 'minimum' => 1, 'description' => 'Top-k sampling parameter.'], self::KEY_STOP_SEQUENCES => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Stop sequences.'], self::KEY_PRESENCE_PENALTY => ['type' => 'number', 'description' => 'Presence penalty for reducing repetition.'], self::KEY_FREQUENCY_PENALTY => ['type' => 'number', 'description' => 'Frequency penalty for reducing repetition.'], self::KEY_LOGPROBS => ['type' => 'boolean', 'description' => 'Whether to return log probabilities.'], self::KEY_TOP_LOGPROBS => ['type' => 'integer', 'minimum' => 1, 'description' => 'Number of top log probabilities to return.'], self::KEY_FUNCTION_DECLARATIONS => ['type' => 'array', 'items' => FunctionDeclaration::getJsonSchema(), 'description' => 'Function declarations available to the model.'], self::KEY_WEB_SEARCH => WebSearch::getJsonSchema(), self::KEY_OUTPUT_FILE_TYPE => ['type' => 'string', 'enum' => FileTypeEnum::getValues(), 'description' => 'Output file type.'], self::KEY_OUTPUT_MIME_TYPE => ['type' => 'string', 'description' => 'Output MIME type.'], self::KEY_OUTPUT_SCHEMA => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Output schema (JSON schema).'], self::KEY_OUTPUT_MEDIA_ORIENTATION => ['type' => 'string', 'enum' => MediaOrientationEnum::getValues(), 'description' => 'Output media orientation.'], self::KEY_OUTPUT_MEDIA_ASPECT_RATIO => ['type' => 'string', 'pattern' => '^\d+:\d+$', 'description' => 'Output media aspect ratio.'], self::KEY_OUTPUT_SPEECH_VOICE => ['type' => 'string', 'description' => 'Output speech voice.'], self::KEY_CUSTOM_OPTIONS => ['type' => 'object', 'additionalProperties' => \true, 'description' => 'Custom provider-specific options.']], 'additionalProperties' => \false];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelConfigArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->outputModalities !== null) {
            $data[self::KEY_OUTPUT_MODALITIES] = array_map(static function (ModalityEnum $modality): string {
                return $modality->value;
            }, $this->outputModalities);
        }
        if ($this->systemInstruction !== null) {
            $data[self::KEY_SYSTEM_INSTRUCTION] = $this->systemInstruction;
        }
        if ($this->candidateCount !== null) {
            $data[self::KEY_CANDIDATE_COUNT] = $this->candidateCount;
        }
        if ($this->maxTokens !== null) {
            $data[self::KEY_MAX_TOKENS] = $this->maxTokens;
        }
        if ($this->temperature !== null) {
            $data[self::KEY_TEMPERATURE] = $this->temperature;
        }
        if ($this->topP !== null) {
            $data[self::KEY_TOP_P] = $this->topP;
        }
        if ($this->topK !== null) {
            $data[self::KEY_TOP_K] = $this->topK;
        }
        if ($this->stopSequences !== null) {
            $data[self::KEY_STOP_SEQUENCES] = $this->stopSequences;
        }
        if ($this->presencePenalty !== null) {
            $data[self::KEY_PRESENCE_PENALTY] = $this->presencePenalty;
        }
        if ($this->frequencyPenalty !== null) {
            $data[self::KEY_FREQUENCY_PENALTY] = $this->frequencyPenalty;
        }
        if ($this->logprobs !== null) {
            $data[self::KEY_LOGPROBS] = $this->logprobs;
        }
        if ($this->topLogprobs !== null) {
            $data[self::KEY_TOP_LOGPROBS] = $this->topLogprobs;
        }
        if ($this->functionDeclarations !== null) {
            $data[self::KEY_FUNCTION_DECLARATIONS] = array_map(static function (FunctionDeclaration $functionDeclaration): array {
                return $functionDeclaration->toArray();
            }, $this->functionDeclarations);
        }
        if ($this->webSearch !== null) {
            $data[self::KEY_WEB_SEARCH] = $this->webSearch->toArray();
        }
        if ($this->outputFileType !== null) {
            $data[self::KEY_OUTPUT_FILE_TYPE] = $this->outputFileType->value;
        }
        if ($this->outputMimeType !== null) {
            $data[self::KEY_OUTPUT_MIME_TYPE] = $this->outputMimeType;
        }
        if ($this->outputSchema !== null) {
            $data[self::KEY_OUTPUT_SCHEMA] = $this->outputSchema;
        }
        if ($this->outputMediaOrientation !== null) {
            $data[self::KEY_OUTPUT_MEDIA_ORIENTATION] = $this->outputMediaOrientation->value;
        }
        if ($this->outputMediaAspectRatio !== null) {
            $data[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO] = $this->outputMediaAspectRatio;
        }
        if ($this->outputSpeechVoice !== null) {
            $data[self::KEY_OUTPUT_SPEECH_VOICE] = $this->outputSpeechVoice;
        }
        if (!empty($this->customOptions)) {
            $data[self::KEY_CUSTOM_OPTIONS] = $this->customOptions;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        $config = new self();
        if (isset($array[self::KEY_OUTPUT_MODALITIES])) {
            $config->setOutputModalities(array_map(static fn(string $modality): ModalityEnum => ModalityEnum::from($modality), $array[self::KEY_OUTPUT_MODALITIES]));
        }
        if (isset($array[self::KEY_SYSTEM_INSTRUCTION])) {
            $config->setSystemInstruction($array[self::KEY_SYSTEM_INSTRUCTION]);
        }
        if (isset($array[self::KEY_CANDIDATE_COUNT])) {
            $config->setCandidateCount($array[self::KEY_CANDIDATE_COUNT]);
        }
        if (isset($array[self::KEY_MAX_TOKENS])) {
            $config->setMaxTokens($array[self::KEY_MAX_TOKENS]);
        }
        if (isset($array[self::KEY_TEMPERATURE])) {
            $config->setTemperature($array[self::KEY_TEMPERATURE]);
        }
        if (isset($array[self::KEY_TOP_P])) {
            $config->setTopP($array[self::KEY_TOP_P]);
        }
        if (isset($array[self::KEY_TOP_K])) {
            $config->setTopK($array[self::KEY_TOP_K]);
        }
        if (isset($array[self::KEY_STOP_SEQUENCES])) {
            $config->setStopSequences($array[self::KEY_STOP_SEQUENCES]);
        }
        if (isset($array[self::KEY_PRESENCE_PENALTY])) {
            $config->setPresencePenalty($array[self::KEY_PRESENCE_PENALTY]);
        }
        if (isset($array[self::KEY_FREQUENCY_PENALTY])) {
            $config->setFrequencyPenalty($array[self::KEY_FREQUENCY_PENALTY]);
        }
        if (isset($array[self::KEY_LOGPROBS])) {
            $config->setLogprobs($array[self::KEY_LOGPROBS]);
        }
        if (isset($array[self::KEY_TOP_LOGPROBS])) {
            $config->setTopLogprobs($array[self::KEY_TOP_LOGPROBS]);
        }
        if (isset($array[self::KEY_FUNCTION_DECLARATIONS])) {
            $config->setFunctionDeclarations(array_map(static function (array $functionDeclarationData): FunctionDeclaration {
                return FunctionDeclaration::fromArray($functionDeclarationData);
            }, $array[self::KEY_FUNCTION_DECLARATIONS]));
        }
        if (isset($array[self::KEY_WEB_SEARCH])) {
            $config->setWebSearch(WebSearch::fromArray($array[self::KEY_WEB_SEARCH]));
        }
        if (isset($array[self::KEY_OUTPUT_FILE_TYPE])) {
            $config->setOutputFileType(FileTypeEnum::from($array[self::KEY_OUTPUT_FILE_TYPE]));
        }
        if (isset($array[self::KEY_OUTPUT_MIME_TYPE])) {
            $config->setOutputMimeType($array[self::KEY_OUTPUT_MIME_TYPE]);
        }
        if (isset($array[self::KEY_OUTPUT_SCHEMA])) {
            $config->setOutputSchema($array[self::KEY_OUTPUT_SCHEMA]);
        }
        if (isset($array[self::KEY_OUTPUT_MEDIA_ORIENTATION])) {
            $config->setOutputMediaOrientation(MediaOrientationEnum::from($array[self::KEY_OUTPUT_MEDIA_ORIENTATION]));
        }
        if (isset($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO])) {
            $config->setOutputMediaAspectRatio($array[self::KEY_OUTPUT_MEDIA_ASPECT_RATIO]);
        }
        if (isset($array[self::KEY_OUTPUT_SPEECH_VOICE])) {
            $config->setOutputSpeechVoice($array[self::KEY_OUTPUT_SPEECH_VOICE]);
        }
        if (isset($array[self::KEY_CUSTOM_OPTIONS])) {
            $config->setCustomOptions($array[self::KEY_CUSTOM_OPTIONS]);
        }
        return $config;
    }
}
                                                                                                                                                                                                                                                                                                                                                            Providers/Models/DTO/ModelRequirements.php                                                          0000644                 00000036511 15227644447 0014574 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\Enums\OptionEnum;
/**
 * Represents requirements that implementing code has for AI model selection.
 *
 * This class defines the capabilities and options that a model must support
 * in order to be considered suitable for the implementing code's needs.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type RequiredOptionArrayShape from RequiredOption
 *
 * @phpstan-type ModelRequirementsArrayShape array{
 *     requiredCapabilities: list<string>,
 *     requiredOptions: list<RequiredOptionArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ModelRequirementsArrayShape>
 */
class ModelRequirements extends AbstractDataTransferObject
{
    public const KEY_REQUIRED_CAPABILITIES = 'requiredCapabilities';
    public const KEY_REQUIRED_OPTIONS = 'requiredOptions';
    /**
     * @var list<CapabilityEnum> The capabilities that the model must support.
     */
    protected array $requiredCapabilities;
    /**
     * @var list<RequiredOption> The options that the model must support with specific values.
     */
    protected array $requiredOptions;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param list<CapabilityEnum> $requiredCapabilities The capabilities that the model must support.
     * @param list<RequiredOption> $requiredOptions The options that the model must support with specific values.
     *
     * @throws InvalidArgumentException If arrays are not lists.
     */
    public function __construct(array $requiredCapabilities, array $requiredOptions)
    {
        if (!array_is_list($requiredCapabilities)) {
            throw new InvalidArgumentException('Required capabilities must be a list array.');
        }
        if (!array_is_list($requiredOptions)) {
            throw new InvalidArgumentException('Required options must be a list array.');
        }
        $this->requiredCapabilities = $requiredCapabilities;
        $this->requiredOptions = $requiredOptions;
    }
    /**
     * Gets the capabilities that the model must support.
     *
     * @since 0.1.0
     *
     * @return list<CapabilityEnum> The required capabilities.
     */
    public function getRequiredCapabilities(): array
    {
        return $this->requiredCapabilities;
    }
    /**
     * Gets the options that the model must support with specific values.
     *
     * @since 0.1.0
     *
     * @return list<RequiredOption> The required options.
     */
    public function getRequiredOptions(): array
    {
        return $this->requiredOptions;
    }
    /**
     * Checks whether the given model metadata meets these requirements.
     *
     * @since 0.2.0
     *
     * @param ModelMetadata $metadata The model metadata to check against.
     * @return bool True if the model meets all requirements, false otherwise.
     */
    public function areMetBy(\WordPress\AiClient\Providers\Models\DTO\ModelMetadata $metadata): bool
    {
        // Create lookup maps for better performance (instead of nested foreach loops)
        $capabilitiesMap = [];
        foreach ($metadata->getSupportedCapabilities() as $capability) {
            $capabilitiesMap[$capability->value] = $capability;
        }
        $optionsMap = [];
        foreach ($metadata->getSupportedOptions() as $option) {
            $optionsMap[$option->getName()->value] = $option;
        }
        // Check if all required capabilities are supported using map lookup
        foreach ($this->requiredCapabilities as $requiredCapability) {
            if (!isset($capabilitiesMap[$requiredCapability->value])) {
                return \false;
            }
        }
        // Check if all required options are supported with the specified values
        foreach ($this->requiredOptions as $requiredOption) {
            // Use map lookup instead of linear search
            if (!isset($optionsMap[$requiredOption->getName()->value])) {
                return \false;
            }
            $supportedOption = $optionsMap[$requiredOption->getName()->value];
            // Check if the required value is supported by this option
            if (!$supportedOption->isSupportedValue($requiredOption->getValue())) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Creates ModelRequirements from prompt data and model configuration.
     *
     * @since 0.2.0
     *
     * @param CapabilityEnum $capability The capability the model must support.
     * @param list<Message> $messages The messages in the conversation.
     * @param ModelConfig $modelConfig The model configuration.
     * @return self The created requirements.
     */
    public static function fromPromptData(CapabilityEnum $capability, array $messages, \WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): self
    {
        // Start with base capability
        $capabilities = [$capability];
        $inputModalities = [];
        // Check if we have chat history (multiple messages)
        if (count($messages) > 1) {
            $capabilities[] = CapabilityEnum::chatHistory();
        }
        // Analyze all messages to determine required input modalities
        $hasFunctionMessageParts = \false;
        foreach ($messages as $message) {
            foreach ($message->getParts() as $part) {
                // Check for text input
                if ($part->getType()->isText()) {
                    $inputModalities[] = ModalityEnum::text();
                }
                // Check for file inputs
                if ($part->getType()->isFile()) {
                    $file = $part->getFile();
                    if ($file !== null) {
                        if ($file->isImage()) {
                            $inputModalities[] = ModalityEnum::image();
                        } elseif ($file->isAudio()) {
                            $inputModalities[] = ModalityEnum::audio();
                        } elseif ($file->isVideo()) {
                            $inputModalities[] = ModalityEnum::video();
                        } elseif ($file->isDocument() || $file->isText()) {
                            $inputModalities[] = ModalityEnum::document();
                        }
                    }
                }
                // Check for function calls/responses (these might require special capabilities)
                if ($part->getType()->isFunctionCall() || $part->getType()->isFunctionResponse()) {
                    $hasFunctionMessageParts = \true;
                }
            }
        }
        // Convert ModelConfig to RequiredOptions
        $requiredOptions = self::toRequiredOptions($modelConfig);
        // Add additional options based on message analysis
        if ($hasFunctionMessageParts) {
            $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true));
        }
        // Add input modalities if we have any inputs
        if (!empty($inputModalities)) {
            // Remove duplicates
            $inputModalities = array_unique($inputModalities, \SORT_REGULAR);
            $requiredOptions = self::includeInRequiredOptions($requiredOptions, new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::inputModalities(), array_values($inputModalities)));
        }
        // Step 6: Return new ModelRequirements
        return new self($capabilities, $requiredOptions);
    }
    /**
     * Converts ModelConfig to an array of RequiredOptions.
     *
     * @since 0.2.0
     *
     * @param ModelConfig $modelConfig The model configuration.
     * @return list<RequiredOption> The required options.
     */
    private static function toRequiredOptions(\WordPress\AiClient\Providers\Models\DTO\ModelConfig $modelConfig): array
    {
        $requiredOptions = [];
        // Map properties that have corresponding OptionEnum values
        if ($modelConfig->getOutputModalities() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputModalities(), $modelConfig->getOutputModalities());
        }
        if ($modelConfig->getSystemInstruction() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::systemInstruction(), $modelConfig->getSystemInstruction());
        }
        if ($modelConfig->getCandidateCount() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::candidateCount(), $modelConfig->getCandidateCount());
        }
        if ($modelConfig->getMaxTokens() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::maxTokens(), $modelConfig->getMaxTokens());
        }
        if ($modelConfig->getTemperature() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::temperature(), $modelConfig->getTemperature());
        }
        if ($modelConfig->getTopP() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topP(), $modelConfig->getTopP());
        }
        if ($modelConfig->getTopK() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topK(), $modelConfig->getTopK());
        }
        if ($modelConfig->getOutputMimeType() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMimeType(), $modelConfig->getOutputMimeType());
        }
        if ($modelConfig->getOutputSchema() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputSchema(), $modelConfig->getOutputSchema());
        }
        // Handle properties without OptionEnum values as custom options
        if ($modelConfig->getStopSequences() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::stopSequences(), $modelConfig->getStopSequences());
        }
        if ($modelConfig->getPresencePenalty() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::presencePenalty(), $modelConfig->getPresencePenalty());
        }
        if ($modelConfig->getFrequencyPenalty() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::frequencyPenalty(), $modelConfig->getFrequencyPenalty());
        }
        if ($modelConfig->getLogprobs() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::logprobs(), $modelConfig->getLogprobs());
        }
        if ($modelConfig->getTopLogprobs() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::topLogprobs(), $modelConfig->getTopLogprobs());
        }
        if ($modelConfig->getFunctionDeclarations() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::functionDeclarations(), \true);
        }
        if ($modelConfig->getWebSearch() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::webSearch(), \true);
        }
        if ($modelConfig->getOutputFileType() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputFileType(), $modelConfig->getOutputFileType());
        }
        if ($modelConfig->getOutputMediaOrientation() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaOrientation(), $modelConfig->getOutputMediaOrientation());
        }
        if ($modelConfig->getOutputMediaAspectRatio() !== null) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::outputMediaAspectRatio(), $modelConfig->getOutputMediaAspectRatio());
        }
        // Add custom options as individual RequiredOptions
        foreach ($modelConfig->getCustomOptions() as $key => $value) {
            $requiredOptions[] = new \WordPress\AiClient\Providers\Models\DTO\RequiredOption(OptionEnum::customOptions(), [$key => $value]);
        }
        return $requiredOptions;
    }
    /**
     * Includes a RequiredOption in the array, ensuring no duplicates based on option name.
     *
     * @since 0.2.0
     *
     * @param list<RequiredOption> $requiredOptions The existing required options.
     * @param RequiredOption $newOption The new option to include.
     * @return list<RequiredOption> The updated required options array.
     */
    private static function includeInRequiredOptions(array $requiredOptions, \WordPress\AiClient\Providers\Models\DTO\RequiredOption $newOption): array
    {
        // Check if we already have this option name
        foreach ($requiredOptions as $index => $existingOption) {
            if ($existingOption->getName()->equals($newOption->getName())) {
                // Replace existing option with new one
                $requiredOptions[$index] = $newOption;
                return $requiredOptions;
            }
        }
        // Option not found, add it
        $requiredOptions[] = $newOption;
        return $requiredOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_REQUIRED_CAPABILITIES => ['type' => 'array', 'items' => ['type' => 'string', 'enum' => CapabilityEnum::getValues()], 'description' => 'The capabilities that the model must support.'], self::KEY_REQUIRED_OPTIONS => ['type' => 'array', 'items' => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::getJsonSchema(), 'description' => 'The options that the model must support with specific values.']], 'required' => [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ModelRequirementsArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_REQUIRED_CAPABILITIES => array_map(static fn(CapabilityEnum $capability): string => $capability->value, $this->requiredCapabilities), self::KEY_REQUIRED_OPTIONS => array_map(static fn(\WordPress\AiClient\Providers\Models\DTO\RequiredOption $option): array => $option->toArray(), $this->requiredOptions)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_REQUIRED_CAPABILITIES, self::KEY_REQUIRED_OPTIONS]);
        return new self(array_map(static fn(string $capability): CapabilityEnum => CapabilityEnum::from($capability), $array[self::KEY_REQUIRED_CAPABILITIES]), array_map(static fn(array $optionData): \WordPress\AiClient\Providers\Models\DTO\RequiredOption => \WordPress\AiClient\Providers\Models\DTO\RequiredOption::fromArray($optionData), $array[self::KEY_REQUIRED_OPTIONS]));
    }
}
                                                                                                                                                                                       Providers/Models/ImageGeneration/Contracts/ImageGenerationOperationModelInterface.php               0000644                 00000001436 15227644447 0025217 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\ImageGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous image generation operations.
 *
 * Provides methods for initiating long-running image generation tasks.
 *
 * @since 0.1.0
 */
interface ImageGenerationOperationModelInterface
{
    /**
     * Creates an image generation operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the image generation prompt.
     * @return GenerativeAiOperation The initiated image generation operation.
     */
    public function generateImageOperation(array $prompt): GenerativeAiOperation;
}
                                                                                                                                                                                                                                  Providers/Models/ImageGeneration/Contracts/ImageGenerationModelInterface.php                        0000644                 00000001342 15227644447 0023332 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\ImageGeneration\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support image generation.
 *
 * Provides synchronous methods for generating images from text prompts.
 *
 * @since 0.1.0
 */
interface ImageGenerationModelInterface
{
    /**
     * Generates images from a prompt.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the image generation prompt.
     * @return GenerativeAiResult Result containing generated images.
     */
    public function generateImageResult(array $prompt): GenerativeAiResult;
}
                                                                                                                                                                                                                                                                                              Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionOperationModelInterface.php 0000644                 00000001527 15227644447 0030176 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Operations\DTO\GenerativeAiOperation;
/**
 * Interface for models that support asynchronous text-to-speech conversion operations.
 *
 * Provides methods for initiating long-running text-to-speech conversion tasks.
 *
 * @since 0.1.0
 */
interface TextToSpeechConversionOperationModelInterface
{
    /**
     * Creates a text-to-speech conversion operation.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text to convert to speech.
     * @return GenerativeAiOperation The initiated text-to-speech conversion operation.
     */
    public function convertTextToSpeechOperation(array $prompt): GenerativeAiOperation;
}
                                                                                                                                                                         Providers/Models/TextToSpeechConversion/Contracts/TextToSpeechConversionModelInterface.php          0000644                 00000001374 15227644447 0026315 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts;

use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
/**
 * Interface for models that support text-to-speech conversion.
 *
 * Provides synchronous methods for converting text to speech audio.
 *
 * @since 0.1.0
 */
interface TextToSpeechConversionModelInterface
{
    /**
     * Converts text to speech.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt Array of messages containing the text to convert to speech.
     * @return GenerativeAiResult Result containing generated speech audio.
     */
    public function convertTextToSpeechResult(array $prompt): GenerativeAiResult;
}
                                                                                                                                                                                                                                                                    Providers/Contracts/ProviderInterface.php                                                           0000644                 00000003322 15227644447 0014624 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
/**
 * Interface for AI providers.
 *
 * Providers represent AI services (Google, OpenAI, Anthropic, etc.)
 * and provide access to models, metadata, and availability information.
 *
 * @since 0.1.0
 */
interface ProviderInterface
{
    /**
     * Gets provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata Provider metadata.
     */
    public static function metadata(): ProviderMetadata;
    /**
     * Creates a model instance.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @param ?ModelConfig $modelConfig Model configuration.
     * @return ModelInterface Model instance.
     * @throws InvalidArgumentException If model not found or configuration invalid.
     */
    public static function model(string $modelId, ?ModelConfig $modelConfig = null): ModelInterface;
    /**
     * Gets provider availability checker.
     *
     * @since 0.1.0
     *
     * @return ProviderAvailabilityInterface Provider availability checker.
     */
    public static function availability(): \WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
    /**
     * Gets model metadata directory.
     *
     * @since 0.1.0
     *
     * @return ModelMetadataDirectoryInterface Model metadata directory.
     */
    public static function modelMetadataDirectory(): \WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
}
                                                                                                                                                                                                                                                                                                              Providers/Contracts/ProviderOperationsHandlerInterface.php                                          0000644                 00000001522 15227644447 0020166 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Operations\Contracts\OperationInterface;
/**
 * Interface for handling provider-level operations.
 *
 * Provides methods to retrieve and manage long-running operations
 * across all models within a provider. Operations are tracked at the
 * provider level rather than per-model.
 *
 * @since 0.1.0
 */
interface ProviderOperationsHandlerInterface
{
    /**
     * Gets an operation by ID.
     *
     * @since 0.1.0
     *
     * @param string $operationId Operation identifier.
     * @return OperationInterface The operation.
     * @throws InvalidArgumentException If operation not found.
     */
    public function getOperation(string $operationId): OperationInterface;
}
                                                                                                                                                                              Providers/Contracts/ProviderWithOperationsHandlerInterface.php                                      0000644                 00000001235 15227644447 0021023 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

/**
 * Interface for providers that support operations handlers.
 *
 * Providers implementing this interface can return an operations handler
 * for managing long-running operations across all their models.
 *
 * @since 0.1.0
 */
interface ProviderWithOperationsHandlerInterface
{
    /**
     * Gets the operations handler for this provider.
     *
     * @since 0.1.0
     *
     * @return ProviderOperationsHandlerInterface The operations handler.
     */
    public static function operationsHandler(): \WordPress\AiClient\Providers\Contracts\ProviderOperationsHandlerInterface;
}
                                                                                                                                                                                                                                                                                                                                                                   Providers/Contracts/ProviderAvailabilityInterface.php                                               0000644                 00000001056 15227644447 0017161 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

/**
 * Interface for checking provider availability.
 *
 * Determines whether a provider is configured and available
 * for use based on API keys, credentials, or other requirements.
 *
 * @since 0.1.0
 */
interface ProviderAvailabilityInterface
{
    /**
     * Checks if the provider is configured.
     *
     * @since 0.1.0
     *
     * @return bool True if the provider is configured and available, false otherwise.
     */
    public function isConfigured(): bool;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  Providers/Contracts/ModelMetadataDirectoryInterface.php                                             0000644                 00000002347 15227644447 0017426 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Contracts;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Interface for accessing model metadata within a provider.
 *
 * Provides methods to list, check, and retrieve model metadata
 * for all models supported by a provider.
 *
 * @since 0.1.0
 */
interface ModelMetadataDirectoryInterface
{
    /**
     * Lists all available model metadata.
     *
     * @since 0.1.0
     *
     * @return list<ModelMetadata> Array of model metadata.
     */
    public function listModelMetadata(): array;
    /**
     * Checks if metadata exists for a specific model.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @return bool True if metadata exists, false otherwise.
     */
    public function hasModelMetadata(string $modelId): bool;
    /**
     * Gets metadata for a specific model.
     *
     * @since 0.1.0
     *
     * @param string $modelId Model identifier.
     * @return ModelMetadata Model metadata.
     * @throws InvalidArgumentException If model metadata not found.
     */
    public function getModelMetadata(string $modelId): ModelMetadata;
}
                                                                                                                                                                                                                                                                                         Providers/Http/Collections/HeadersCollection.php                                                    0000644                 00000007411 15227644447 0016040 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Collections;

/**
 * Simple collection for managing HTTP headers with case-insensitive access.
 *
 * This class stores HTTP headers while preserving their original casing
 * and provides efficient case-insensitive lookups.
 *
 * @since 0.1.0
 */
class HeadersCollection
{
    /**
     * @var array<string, list<string>> The headers with original casing.
     */
    private array $headers = [];
    /**
     * @var array<string, string> Map of lowercase header names to actual header names.
     */
    private array $headersMap = [];
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param array<string, string|list<string>> $headers Initial headers.
     */
    public function __construct(array $headers = [])
    {
        foreach ($headers as $name => $value) {
            $this->set($name, $value);
        }
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function get(string $name): ?array
    {
        $lowerName = strtolower($name);
        if (!isset($this->headersMap[$lowerName])) {
            return null;
        }
        $actualName = $this->headersMap[$lowerName];
        return $this->headers[$actualName];
    }
    /**
     * Gets all headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> All headers with their original casing.
     */
    public function getAll(): array
    {
        return $this->headers;
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string or null if not found.
     */
    public function getAsString(string $name): ?string
    {
        $values = $this->get($name);
        return $values !== null ? implode(', ', $values) : null;
    }
    /**
     * Checks if a header exists.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return bool True if the header exists, false otherwise.
     */
    public function has(string $name): bool
    {
        return isset($this->headersMap[strtolower($name)]);
    }
    /**
     * Sets a header value, replacing any existing value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return void
     */
    private function set(string $name, $value): void
    {
        if (is_array($value)) {
            $normalizedValues = array_values($value);
        } else {
            // Split comma-separated string into array
            $normalizedValues = array_map('trim', explode(',', $value));
        }
        $lowerName = strtolower($name);
        // If header exists with different casing, remove the old casing
        if (isset($this->headersMap[$lowerName])) {
            $oldName = $this->headersMap[$lowerName];
            if ($oldName !== $name) {
                unset($this->headers[$oldName]);
            }
        }
        // Always use the new casing
        $this->headers[$name] = $normalizedValues;
        $this->headersMap[$lowerName] = $name;
    }
    /**
     * Returns a new instance with the specified header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return self A new instance with the header.
     */
    public function withHeader(string $name, $value): self
    {
        $new = clone $this;
        $new->set($name, $value);
        return $new;
    }
}
                                                                                                                                                                                                                                                       Providers/Http/HttpTransporter.php                                                                  0000644                 00000025234 15227644447 0013361 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http;

use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\ClientWithOptionsInterface;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Exception\NetworkException;
/**
 * HTTP transporter implementation using HTTPlug.
 *
 * This class handles the conversion between custom Request/Response
 * objects and PSR-7 messages, using HTTPlug for client abstraction
 * and PSR-17 factories for message creation.
 *
 * @since 0.1.0
 */
class HttpTransporter implements HttpTransporterInterface
{
    /**
     * @var RequestFactoryInterface PSR-17 request factory.
     */
    private RequestFactoryInterface $requestFactory;
    /**
     * @var StreamFactoryInterface PSR-17 stream factory.
     */
    private StreamFactoryInterface $streamFactory;
    /**
     * @var ClientInterface PSR-18 HTTP client.
     */
    private ClientInterface $client;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ClientInterface|null $client PSR-18 HTTP client.
     * @param RequestFactoryInterface|null $requestFactory PSR-17 request factory.
     * @param StreamFactoryInterface|null $streamFactory PSR-17 stream factory.
     */
    public function __construct(?ClientInterface $client = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null)
    {
        $this->client = $client ?: Psr18ClientDiscovery::find();
        $this->requestFactory = $requestFactory ?: Psr17FactoryDiscovery::findRequestFactory();
        $this->streamFactory = $streamFactory ?: Psr17FactoryDiscovery::findStreamFactory();
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 0.2.0 Added optional RequestOptions parameter and ClientWithOptions support.
     */
    public function send(Request $request, ?RequestOptions $options = null): Response
    {
        $psr7Request = $this->convertToPsr7Request($request);
        // Merge request options with parameter options, with parameter options taking precedence
        $mergedOptions = $this->mergeOptions($request->getOptions(), $options);
        try {
            $hasOptions = $mergedOptions !== null;
            if ($hasOptions && $this->client instanceof ClientWithOptionsInterface) {
                $psr7Response = $this->client->sendRequestWithOptions($psr7Request, $mergedOptions);
            } elseif ($hasOptions && $this->isGuzzleClient($this->client)) {
                $psr7Response = $this->sendWithGuzzle($psr7Request, $mergedOptions);
            } else {
                $psr7Response = $this->client->sendRequest($psr7Request);
            }
        } catch (\WordPress\AiClientDependencies\Psr\Http\Client\NetworkExceptionInterface $e) {
            throw NetworkException::fromPsr18NetworkException($psr7Request, $e);
        } catch (\WordPress\AiClientDependencies\Psr\Http\Client\ClientExceptionInterface $e) {
            // Handle other PSR-18 client exceptions that are not network-related
            throw new RuntimeException(sprintf('HTTP client error occurred while sending request to %s: %s', $request->getUri(), $e->getMessage()), 0, $e);
        }
        return $this->convertFromPsr7Response($psr7Response);
    }
    /**
     * Merges request options with parameter options taking precedence.
     *
     * @since 0.2.0
     *
     * @param RequestOptions|null $requestOptions Options from the Request object.
     * @param RequestOptions|null $parameterOptions Options passed as method parameter.
     * @return RequestOptions|null Merged options, or null if both are null.
     */
    private function mergeOptions(?RequestOptions $requestOptions, ?RequestOptions $parameterOptions): ?RequestOptions
    {
        // If no options at all, return null
        if ($requestOptions === null && $parameterOptions === null) {
            return null;
        }
        // If only one set of options exists, return it
        if ($requestOptions === null) {
            return $parameterOptions;
        }
        if ($parameterOptions === null) {
            return $requestOptions;
        }
        // Both exist, merge them with parameter options taking precedence
        $merged = new RequestOptions();
        // Start with request options (lower precedence)
        if ($requestOptions->getTimeout() !== null) {
            $merged->setTimeout($requestOptions->getTimeout());
        }
        if ($requestOptions->getConnectTimeout() !== null) {
            $merged->setConnectTimeout($requestOptions->getConnectTimeout());
        }
        if ($requestOptions->getMaxRedirects() !== null) {
            $merged->setMaxRedirects($requestOptions->getMaxRedirects());
        }
        // Override with parameter options (higher precedence)
        if ($parameterOptions->getTimeout() !== null) {
            $merged->setTimeout($parameterOptions->getTimeout());
        }
        if ($parameterOptions->getConnectTimeout() !== null) {
            $merged->setConnectTimeout($parameterOptions->getConnectTimeout());
        }
        if ($parameterOptions->getMaxRedirects() !== null) {
            $merged->setMaxRedirects($parameterOptions->getMaxRedirects());
        }
        return $merged;
    }
    /**
     * Determines if the underlying client matches the Guzzle client shape.
     *
     * @since 0.2.0
     *
     * @param ClientInterface $client The HTTP client instance.
     * @return bool True when the client exposes Guzzle's send signature.
     */
    private function isGuzzleClient(ClientInterface $client): bool
    {
        $reflection = new \ReflectionObject($client);
        if (!is_callable([$client, 'send'])) {
            return \false;
        }
        if (!$reflection->hasMethod('send')) {
            return \false;
        }
        $method = $reflection->getMethod('send');
        if (!$method->isPublic() || $method->isStatic()) {
            return \false;
        }
        $parameters = $method->getParameters();
        if (count($parameters) < 2) {
            return \false;
        }
        $firstParameter = $parameters[0]->getType();
        if (!$firstParameter instanceof \ReflectionNamedType || $firstParameter->isBuiltin()) {
            return \false;
        }
        if (!is_a($firstParameter->getName(), RequestInterface::class, \true)) {
            return \false;
        }
        $secondParameter = $parameters[1];
        $secondType = $secondParameter->getType();
        if (!$secondType instanceof \ReflectionNamedType || $secondType->getName() !== 'array') {
            return \false;
        }
        return \true;
    }
    /**
     * Sends a request using a Guzzle-compatible client.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $request The PSR-7 request to send.
     * @param RequestOptions $options The request options.
     * @return ResponseInterface The PSR-7 response received.
     */
    private function sendWithGuzzle(RequestInterface $request, RequestOptions $options): ResponseInterface
    {
        $guzzleOptions = $this->buildGuzzleOptions($options);
        /** @var callable $callable */
        $callable = [$this->client, 'send'];
        /** @var ResponseInterface $response */
        $response = $callable($request, $guzzleOptions);
        return $response;
    }
    /**
     * Converts request options to a Guzzle-compatible options array.
     *
     * @since 0.2.0
     *
     * @param RequestOptions $options The request options.
     * @return array<string, mixed> Guzzle-compatible options.
     */
    private function buildGuzzleOptions(RequestOptions $options): array
    {
        $guzzleOptions = [];
        $timeout = $options->getTimeout();
        if ($timeout !== null) {
            $guzzleOptions['timeout'] = $timeout;
        }
        $connectTimeout = $options->getConnectTimeout();
        if ($connectTimeout !== null) {
            $guzzleOptions['connect_timeout'] = $connectTimeout;
        }
        $allowRedirects = $options->allowsRedirects();
        if ($allowRedirects !== null) {
            if ($allowRedirects) {
                $redirectOptions = [];
                $maxRedirects = $options->getMaxRedirects();
                if ($maxRedirects !== null) {
                    $redirectOptions['max'] = $maxRedirects;
                }
                $guzzleOptions['allow_redirects'] = !empty($redirectOptions) ? $redirectOptions : \true;
            } else {
                $guzzleOptions['allow_redirects'] = \false;
            }
        }
        return $guzzleOptions;
    }
    /**
     * Converts a custom Request to a PSR-7 request.
     *
     * @since 0.1.0
     *
     * @param Request $request The custom request.
     * @return RequestInterface The PSR-7 request.
     */
    private function convertToPsr7Request(Request $request): RequestInterface
    {
        $psr7Request = $this->requestFactory->createRequest($request->getMethod()->value, $request->getUri());
        // Add headers
        foreach ($request->getHeaders() as $name => $values) {
            foreach ($values as $value) {
                $psr7Request = $psr7Request->withAddedHeader($name, $value);
            }
        }
        // Add body if present
        $body = $request->getBody();
        if ($body !== null) {
            $stream = $this->streamFactory->createStream($body);
            $psr7Request = $psr7Request->withBody($stream);
        }
        return $psr7Request;
    }
    /**
     * Converts a PSR-7 response to a custom Response.
     *
     * @since 0.1.0
     *
     * @param ResponseInterface $psr7Response The PSR-7 response.
     * @return Response The custom response.
     */
    private function convertFromPsr7Response(ResponseInterface $psr7Response): Response
    {
        $body = (string) $psr7Response->getBody();
        // PSR-7 always returns headers as arrays, but HeadersCollection handles this
        return new Response(
            $psr7Response->getStatusCode(),
            $psr7Response->getHeaders(),
            // @phpstan-ignore-line
            $body === '' ? null : $body
        );
    }
}
                                                                                                                                                                                                                                                                                                                                                                    Providers/Http/HttpTransporterFactory.php                                                           0000644                 00000002026 15227644447 0014703 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http;

use WordPress\AiClientDependencies\Http\Discovery\Psr17FactoryDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
/**
 * Factory for creating HTTP transporters.
 *
 * Uses HTTPlug's Discovery component to automatically find
 * available HTTP clients and factories.
 *
 * @since 0.1.0
 */
class HttpTransporterFactory
{
    /**
     * Creates an HTTP transporter.
     *
     * Uses HTTPlug Discovery to automatically find PSR-18 client
     * and PSR-17 factories if not provided.
     *
     * @since 0.1.0
     *
     * @return HttpTransporterInterface The HTTP transporter.
     */
    public static function createTransporter(): HttpTransporterInterface
    {
        return new \WordPress\AiClient\Providers\Http\HttpTransporter(Psr18ClientDiscovery::find(), Psr17FactoryDiscovery::findRequestFactory(), Psr17FactoryDiscovery::findStreamFactory());
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          Providers/Http/Contracts/ClientWithOptionsInterface.php                                             0000644                 00000002001 15227644447 0017370 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClientDependencies\Psr\Http\Message\ResponseInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
/**
 * Interface for HTTP clients that support per-request transport options.
 *
 * Extends the capabilities of PSR-18 clients by allowing custom transport
 * configuration such as timeouts and redirect handling on each request.
 *
 * @since 0.2.0
 */
interface ClientWithOptionsInterface
{
    /**
     * Sends an HTTP request with the given transport options.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $request The PSR-7 request to send.
     * @param RequestOptions $options The request transport options. Must not be null.
     * @return ResponseInterface The PSR-7 response received.
     */
    public function sendRequestWithOptions(RequestInterface $request, RequestOptions $options): ResponseInterface;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               Providers/Http/Contracts/WithHttpTransporterInterface.php                                           0000644                 00000001455 15227644447 0017775 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

/**
 * Interface for models that require HTTP transport capabilities.
 *
 * @since 0.1.0
 */
interface WithHttpTransporterInterface
{
    /**
     * Sets the HTTP transporter.
     *
     * @since 0.1.0
     *
     * @param HttpTransporterInterface $transporter The HTTP transporter instance.
     * @return void
     */
    public function setHttpTransporter(\WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface $transporter): void;
    /**
     * Returns the HTTP transporter.
     *
     * @since 0.1.0
     *
     * @return HttpTransporterInterface The HTTP transporter instance.
     */
    public function getHttpTransporter(): \WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
}
                                                                                                                                                                                                                   Providers/Http/Contracts/error_log                                                                  0000644                 00000001546 15227644447 0013342 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 03:31:49 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Contracts/RequestAuthenticationInterface.php:13
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Contracts/RequestAuthenticationInterface.php on line 13
[20-Jul-2026 06:58:01 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Contracts/RequestAuthenticationInterface.php:13
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Contracts/RequestAuthenticationInterface.php on line 13
                                                                                                                                                          Providers/Http/Contracts/WithRequestAuthenticationInterface.php                                     0000644                 00000001540 15227644447 0021135 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

/**
 * Interface for models that support request authentication.
 *
 * @since 0.1.0
 */
interface WithRequestAuthenticationInterface
{
    /**
     * Sets the request authentication.
     *
     * @since 0.1.0
     *
     * @param RequestAuthenticationInterface $authentication The authentication instance.
     * @return void
     */
    public function setRequestAuthentication(\WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface $authentication): void;
    /**
     * Returns the request authentication.
     *
     * @since 0.1.0
     *
     * @return RequestAuthenticationInterface The authentication instance.
     */
    public function getRequestAuthentication(): \WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
}
                                                                                                                                                                Providers/Http/Contracts/RequestAuthenticationInterface.php                                         0000644                 00000001155 15227644447 0020303 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClient\Common\Contracts\WithJsonSchemaInterface;
use WordPress\AiClient\Providers\Http\DTO\Request;
/**
 * Interface for HTTP request authentication.
 *
 * @since 0.1.0
 */
interface RequestAuthenticationInterface extends WithJsonSchemaInterface
{
    /**
     * Authenticates an HTTP request.
     *
     * @since 0.1.0
     *
     * @param Request $request The request to authenticate.
     * @return Request The authenticated request.
     */
    public function authenticateRequest(Request $request): Request;
}
                                                                                                                                                                                                                                                                                                                                                                                                                   Providers/Http/Contracts/HttpTransporterInterface.php                                               0000644                 00000001534 15227644447 0017137 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Contracts;

use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\DTO\Response;
/**
 * Interface for HTTP transport implementations.
 *
 * Handles sending HTTP requests and receiving responses using
 * PSR-7, PSR-17, and PSR-18 standards internally.
 *
 * @since 0.1.0
 */
interface HttpTransporterInterface
{
    /**
     * Sends an HTTP request and returns the response.
     *
     * @since 0.1.0
     *
     * @param Request $request The request to send.
     * @param RequestOptions|null $options Optional transport options for the request.
     * @return Response The response received.
     */
    public function send(Request $request, ?RequestOptions $options = null): Response;
}
                                                                                                                                                                    Providers/Http/Abstracts/error_log                                                                  0000644                 00000001604 15227644447 0013323 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 05:17:58 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php on line 20
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php:20
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php on line 20
                                                                                                                            Providers/Http/Abstracts/AbstractClientDiscoveryStrategy.php                                        0000644                 00000005557 15227644447 0020447 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Abstracts;

use WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery;
use WordPress\AiClientDependencies\Http\Discovery\Strategy\DiscoveryStrategy;
use WordPress\AiClientDependencies\Nyholm\Psr7\Factory\Psr17Factory;
use WordPress\AiClientDependencies\Psr\Http\Client\ClientInterface;
/**
 * Abstract discovery strategy for HTTP client implementations.
 *
 * Provides a base for registering custom HTTP client implementations
 * with HTTPlug's discovery mechanism. Subclasses must implement
 * the createClient() method to provide their specific PSR-18
 * HTTP client instance using the provided Psr17Factory.
 *
 * @since 1.1.0
 */
abstract class AbstractClientDiscoveryStrategy implements DiscoveryStrategy
{
    /**
     * Initializes and registers the discovery strategy.
     *
     * @since 1.1.0
     *
     * @return void
     */
    public static function init(): void
    {
        if (!class_exists('WordPress\AiClientDependencies\Http\Discovery\Psr18ClientDiscovery')) {
            return;
        }
        Psr18ClientDiscovery::prependStrategy(static::class);
    }
    /**
     * {@inheritDoc}
     *
     * @since 1.1.0
     *
     * @param string $type The type of discovery.
     * @return array<array<string, mixed>> The discovery candidates.
     */
    public static function getCandidates($type)
    {
        if (ClientInterface::class === $type) {
            return [['class' => static function () {
                $psr17Factory = new Psr17Factory();
                return static::createClient($psr17Factory);
            }]];
        }
        $psr17Factories = ['WordPress\AiClientDependencies\Psr\Http\Message\RequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ResponseFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\ServerRequestFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\StreamFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UploadedFileFactoryInterface', 'WordPress\AiClientDependencies\Psr\Http\Message\UriFactoryInterface'];
        if (in_array($type, $psr17Factories, \true)) {
            return [['class' => Psr17Factory::class]];
        }
        return [];
    }
    /**
     * Creates an instance of the HTTP client.
     *
     * Subclasses must implement this method to return their specific
     * PSR-18 HTTP client instance. The provided Psr17Factory implements
     * all PSR-17 interfaces (RequestFactory, ResponseFactory, StreamFactory,
     * etc.) and can be used to satisfy client constructor dependencies.
     *
     * @since 1.1.0
     *
     * @param Psr17Factory $psr17Factory The PSR-17 factory for creating HTTP messages.
     * @return ClientInterface The PSR-18 HTTP client.
     */
    abstract protected static function createClient(Psr17Factory $psr17Factory): ClientInterface;
}
                                                                                                                                                 Providers/Http/Enums/RequestAuthenticationMethod.php                                                0000644                 00000002344 15227644447 0016753 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Enums;

use WordPress\AiClient\Common\AbstractEnum;
use WordPress\AiClient\Common\Contracts\WithArrayTransformationInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\ApiKeyRequestAuthentication;
/**
 * Enum for request authentication methods.
 *
 * @since 0.4.0
 *
 * @method static self apiKey() Creates an instance for API_KEY method.
 * @method bool isApiKey() Checks if the method is API_KEY.
 */
class RequestAuthenticationMethod extends AbstractEnum
{
    /**
     * API key authentication.
     */
    public const API_KEY = 'api_key';
    /**
     * Gets the implementation class for the authentication method.
     *
     * @since 0.4.0
     *
     * @return class-string<RequestAuthenticationInterface&WithArrayTransformationInterface> The implementation class.
     *
     * @phpstan-ignore missingType.generics
     */
    public function getImplementationClass(): string
    {
        // At the moment, this is the only supported method.
        // Once more methods are available, add conditionals here for each method.
        return ApiKeyRequestAuthentication::class;
    }
}
                                                                                                                                                                                                                                                                                            Providers/Http/Enums/HttpMethodEnum.php                                                             0000644                 00000004750 15227644447 0014172 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Represents HTTP request methods.
 *
 * @since 0.1.0
 *
 * @method static self GET()
 * @method static self POST()
 * @method static self PUT()
 * @method static self PATCH()
 * @method static self DELETE()
 * @method static self HEAD()
 * @method static self OPTIONS()
 * @method static self CONNECT()
 * @method static self TRACE()
 *
 * @method bool isGet()
 * @method bool isPost()
 * @method bool isPut()
 * @method bool isPatch()
 * @method bool isDelete()
 * @method bool isHead()
 * @method bool isOptions()
 * @method bool isConnect()
 * @method bool isTrace()
 */
final class HttpMethodEnum extends AbstractEnum
{
    /**
     * GET method for retrieving resources.
     *
     * @var string
     */
    public const GET = 'GET';
    /**
     * POST method for creating resources.
     *
     * @var string
     */
    public const POST = 'POST';
    /**
     * PUT method for updating/replacing resources.
     *
     * @var string
     */
    public const PUT = 'PUT';
    /**
     * PATCH method for partially updating resources.
     *
     * @var string
     */
    public const PATCH = 'PATCH';
    /**
     * DELETE method for removing resources.
     *
     * @var string
     */
    public const DELETE = 'DELETE';
    /**
     * HEAD method for retrieving headers only.
     *
     * @var string
     */
    public const HEAD = 'HEAD';
    /**
     * OPTIONS method for retrieving allowed methods.
     *
     * @var string
     */
    public const OPTIONS = 'OPTIONS';
    /**
     * CONNECT method for establishing tunnel.
     *
     * @var string
     */
    public const CONNECT = 'CONNECT';
    /**
     * TRACE method for diagnostic purposes.
     *
     * @var string
     */
    public const TRACE = 'TRACE';
    /**
     * Checks if this method is idempotent.
     *
     * @since 0.1.0
     *
     * @return bool True if the method is idempotent, false otherwise.
     */
    public function isIdempotent(): bool
    {
        return in_array($this->value, [self::GET, self::HEAD, self::OPTIONS, self::TRACE, self::PUT, self::DELETE], \true);
    }
    /**
     * Checks if this method typically has a request body.
     *
     * @since 0.1.0
     *
     * @return bool True if the method typically has a body, false otherwise.
     */
    public function hasBody(): bool
    {
        return in_array($this->value, [self::POST, self::PUT, self::PATCH], \true);
    }
}
                        Providers/Http/error_log                                                                            0000644                 00000001424 15227644447 0011375 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 08:18:11 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php on line 29
[20-Jul-2026 11:44:10 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php:29
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/HttpTransporter.php on line 29
                                                                                                                                                                                                                                            Providers/Http/Util/ResponseUtil.php                                                                0000644                 00000004146 15227644447 0013546 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Util;

use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Exception\ClientException;
use WordPress\AiClient\Providers\Http\Exception\RedirectException;
use WordPress\AiClient\Providers\Http\Exception\ServerException;
/**
 * Class with static utility methods to process HTTP responses.
 *
 * @since 0.1.0
 */
class ResponseUtil
{
    /**
     * Throws an appropriate exception if the given response is not successful.
     *
     * This method checks the HTTP status code of the response and throws
     * the appropriate exception type based on the status code range:
     * - 3xx: RedirectException (redirect responses)
     * - 4xx: ClientException (client errors)
     * - 5xx: ServerException (server errors)
     * - Other unsuccessful responses: RuntimeException (invalid status codes)
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws RedirectException If the response indicates a redirect (3xx).
     * @throws ClientException If the response indicates a client error (4xx).
     * @throws ServerException If the response indicates a server error (5xx).
     * @throws \RuntimeException If the response has an invalid status code.
     */
    public static function throwIfNotSuccessful(Response $response): void
    {
        if ($response->isSuccessful()) {
            return;
        }
        $statusCode = $response->getStatusCode();
        // 3xx Redirect Responses
        if ($statusCode >= 300 && $statusCode < 400) {
            throw RedirectException::fromRedirectResponse($response);
        }
        // 4xx Client Errors
        if ($statusCode >= 400 && $statusCode < 500) {
            throw ClientException::fromClientErrorResponse($response);
        }
        // 5xx Server Errors
        if ($statusCode >= 500 && $statusCode < 600) {
            throw ServerException::fromServerErrorResponse($response);
        }
        throw new \RuntimeException(sprintf('Response returned invalid status code: %s', $response->getStatusCode()));
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                          Providers/Http/Util/ErrorMessageExtractor.php                                                       0000644                 00000003545 15227644447 0015406 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Util;

/**
 * Utility for extracting error messages from API response data.
 *
 * Centralizes the logic for parsing common API error response formats
 * to avoid code duplication across exception classes.
 *
 * @since 0.2.0
 * @since 0.4.0 Moved from Utilities namespace to Util namespace.
 */
class ErrorMessageExtractor
{
    /**
     * Extracts error message from API response data.
     *
     * Handles common error response formats:
     * - { "error": { "message": "Error text" } }
     * - { "error": "Error text" }
     * - { "message": "Error text" }
     *
     * @since 0.2.0
     *
     * @param mixed $data The response data to extract error message from.
     * @return string|null The extracted error message, or null if none found.
     */
    public static function extractFromResponseData($data): ?string
    {
        if (!is_array($data)) {
            return null;
        }
        // Handle [ { "error": { "message": "Error text" } } ]
        if (isset($data[0]) && is_array($data[0]) && isset($data[0]['error']) && is_array($data[0]['error']) && isset($data[0]['error']['message']) && is_string($data[0]['error']['message'])) {
            return $data[0]['error']['message'];
        }
        // Handle { "error": { "message": "Error text" } }
        if (isset($data['error']) && is_array($data['error']) && isset($data['error']['message']) && is_string($data['error']['message'])) {
            return $data['error']['message'];
        }
        // Handle { "error": "Error text" }
        if (isset($data['error']) && is_string($data['error'])) {
            return $data['error'];
        }
        // Handle { "message": "Error text" }
        if (isset($data['message']) && is_string($data['message'])) {
            return $data['message'];
        }
        return null;
    }
}
                                                                                                                                                           Providers/Http/DTO/ApiKeyRequestAuthentication.php                                                  0000644                 00000004517 15227644447 0016260 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
/**
 * Class for HTTP request authentication using an API key.
 *
 * @since 0.1.0
 *
 * @phpstan-type ApiKeyRequestAuthenticationArrayShape array{
 *     apiKey: string
 * }
 *
 * @extends AbstractDataTransferObject<ApiKeyRequestAuthenticationArrayShape>
 */
class ApiKeyRequestAuthentication extends AbstractDataTransferObject implements RequestAuthenticationInterface
{
    public const KEY_API_KEY = 'apiKey';
    /**
     * @var string The API key used for authentication.
     */
    protected string $apiKey;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param string $apiKey The API key used for authentication.
     */
    public function __construct(string $apiKey)
    {
        $this->apiKey = $apiKey;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function authenticateRequest(\WordPress\AiClient\Providers\Http\DTO\Request $request): \WordPress\AiClient\Providers\Http\DTO\Request
    {
        // Add the API key to the request headers.
        return $request->withHeader('Authorization', 'Bearer ' . $this->apiKey);
    }
    /**
     * Gets the API key.
     *
     * @since 0.1.0
     *
     * @return string The API key.
     */
    public function getApiKey(): string
    {
        return $this->apiKey;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @since 0.1.0
     *
     * @return ApiKeyRequestAuthenticationArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_API_KEY => $this->apiKey];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_API_KEY]);
        return new self($array[self::KEY_API_KEY]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_API_KEY => ['type' => 'string', 'title' => 'API Key', 'description' => 'The API key used for authentication.']], 'required' => [self::KEY_API_KEY]];
    }
}
                                                                                                                                                                                 Providers/Http/DTO/error_log                                                                        0000644                 00000005740 15227644447 0012030 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 16:16:01 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php:31
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php on line 31
[19-Jul-2026 05:17:57 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/ApiKeyRequestAuthentication.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/ApiKeyRequestAuthentication.php on line 19
[19-Jul-2026 05:17:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/RequestOptions.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/RequestOptions.php on line 23
[19-Jul-2026 08:18:08 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php on line 25
[19-Jul-2026 11:58:19 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php:31
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php on line 31
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/ApiKeyRequestAuthentication.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/ApiKeyRequestAuthentication.php on line 19
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/RequestOptions.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/RequestOptions.php on line 23
[20-Jul-2026 11:44:09 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php:25
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php on line 25
                                Providers/Http/DTO/RequestOptions.php                                                               0000644                 00000014523 15227644447 0013627 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
/**
 * Represents optional HTTP transport configuration for a single request.
 *
 * Provides mutable setters for working with timeouts and redirect handling.
 *
 * @since 0.2.0
 *
 * @phpstan-type RequestOptionsArrayShape array{
 *     timeout?: float|null,
 *     connectTimeout?: float|null,
 *     maxRedirects?: int|null
 * }
 *
 * @extends AbstractDataTransferObject<RequestOptionsArrayShape>
 */
class RequestOptions extends AbstractDataTransferObject
{
    public const KEY_TIMEOUT = 'timeout';
    public const KEY_CONNECT_TIMEOUT = 'connectTimeout';
    public const KEY_MAX_REDIRECTS = 'maxRedirects';
    /**
     * @var float|null Maximum duration in seconds to wait for the full response.
     */
    protected ?float $timeout = null;
    /**
     * @var float|null Maximum duration in seconds to wait for the initial connection.
     */
    protected ?float $connectTimeout = null;
    /**
     * @var int|null Maximum number of redirects to follow. 0 disables redirects, null is unspecified.
     */
    protected ?int $maxRedirects = null;
    /**
     * Sets the request timeout in seconds.
     *
     * @since 0.2.0
     *
     * @param float|null $timeout Timeout in seconds.
     * @return void
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    public function setTimeout(?float $timeout): void
    {
        $this->validateTimeout($timeout, self::KEY_TIMEOUT);
        $this->timeout = $timeout;
    }
    /**
     * Sets the connection timeout in seconds.
     *
     * @since 0.2.0
     *
     * @param float|null $timeout Connection timeout in seconds.
     * @return void
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    public function setConnectTimeout(?float $timeout): void
    {
        $this->validateTimeout($timeout, self::KEY_CONNECT_TIMEOUT);
        $this->connectTimeout = $timeout;
    }
    /**
     * Sets the maximum number of redirects to follow.
     *
     * Set to 0 to disable redirects, null for unspecified, or a positive integer
     * to enable redirects with a maximum count.
     *
     * @since 0.2.0
     *
     * @param int|null $maxRedirects Maximum redirects to follow, or 0 to disable, or null for unspecified.
     * @return void
     *
     * @throws InvalidArgumentException When redirect count is negative.
     */
    public function setMaxRedirects(?int $maxRedirects): void
    {
        if ($maxRedirects !== null && $maxRedirects < 0) {
            throw new InvalidArgumentException('Request option "maxRedirects" must be greater than or equal to 0.');
        }
        $this->maxRedirects = $maxRedirects;
    }
    /**
     * Gets the request timeout in seconds.
     *
     * @since 0.2.0
     *
     * @return float|null Timeout in seconds.
     */
    public function getTimeout(): ?float
    {
        return $this->timeout;
    }
    /**
     * Gets the connection timeout in seconds.
     *
     * @since 0.2.0
     *
     * @return float|null Connection timeout in seconds.
     */
    public function getConnectTimeout(): ?float
    {
        return $this->connectTimeout;
    }
    /**
     * Checks whether redirects are allowed.
     *
     * @since 0.2.0
     *
     * @return bool|null True when redirects are allowed (maxRedirects > 0),
     *                   false when disabled (maxRedirects = 0),
     *                   null when unspecified (maxRedirects = null).
     */
    public function allowsRedirects(): ?bool
    {
        if ($this->maxRedirects === null) {
            return null;
        }
        return $this->maxRedirects > 0;
    }
    /**
     * Gets the maximum number of redirects to follow.
     *
     * @since 0.2.0
     *
     * @return int|null Maximum redirects or null when not specified.
     */
    public function getMaxRedirects(): ?int
    {
        return $this->maxRedirects;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     *
     * @return RequestOptionsArrayShape
     */
    public function toArray(): array
    {
        $data = [];
        if ($this->timeout !== null) {
            $data[self::KEY_TIMEOUT] = $this->timeout;
        }
        if ($this->connectTimeout !== null) {
            $data[self::KEY_CONNECT_TIMEOUT] = $this->connectTimeout;
        }
        if ($this->maxRedirects !== null) {
            $data[self::KEY_MAX_REDIRECTS] = $this->maxRedirects;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     */
    public static function fromArray(array $array): self
    {
        $instance = new self();
        if (isset($array[self::KEY_TIMEOUT])) {
            $instance->setTimeout((float) $array[self::KEY_TIMEOUT]);
        }
        if (isset($array[self::KEY_CONNECT_TIMEOUT])) {
            $instance->setConnectTimeout((float) $array[self::KEY_CONNECT_TIMEOUT]);
        }
        if (isset($array[self::KEY_MAX_REDIRECTS])) {
            $instance->setMaxRedirects((int) $array[self::KEY_MAX_REDIRECTS]);
        }
        return $instance;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.2.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the full response.'], self::KEY_CONNECT_TIMEOUT => ['type' => ['number', 'null'], 'minimum' => 0, 'description' => 'Maximum duration in seconds to wait for the initial connection.'], self::KEY_MAX_REDIRECTS => ['type' => ['integer', 'null'], 'minimum' => 0, 'description' => 'Maximum redirects to follow. 0 disables, null is unspecified.']], 'additionalProperties' => \false];
    }
    /**
     * Validates timeout values.
     *
     * @since 0.2.0
     *
     * @param float|null $value Timeout to validate.
     * @param string $fieldName Field name for the error message.
     *
     * @throws InvalidArgumentException When timeout is negative.
     */
    private function validateTimeout(?float $value, string $fieldName): void
    {
        if ($value !== null && $value < 0) {
            throw new InvalidArgumentException(sprintf('Request option "%s" must be greater than or equal to 0.', $fieldName));
        }
    }
}
                                                                                                                                                                             Providers/Http/DTO/Response.php                                                                     0000644                 00000013734 15227644447 0012424 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\Collections\HeadersCollection;
/**
 * Represents an HTTP response.
 *
 * This class encapsulates HTTP response data that has been converted
 * from PSR-7 responses by the HTTP transporter.
 *
 * @since 0.1.0
 *
 * @phpstan-type ResponseArrayShape array{
 *     statusCode: int,
 *     headers: array<string, list<string>>,
 *     body?: string|null
 * }
 *
 * @extends AbstractDataTransferObject<ResponseArrayShape>
 */
class Response extends AbstractDataTransferObject
{
    public const KEY_STATUS_CODE = 'statusCode';
    public const KEY_HEADERS = 'headers';
    public const KEY_BODY = 'body';
    /**
     * @var int The HTTP status code.
     */
    protected int $statusCode;
    /**
     * @var HeadersCollection The response headers.
     */
    protected HeadersCollection $headers;
    /**
     * @var string|null The response body.
     */
    protected ?string $body;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param int $statusCode The HTTP status code.
     * @param array<string, string|list<string>> $headers The response headers.
     * @param string|null $body The response body.
     *
     * @throws InvalidArgumentException If the status code is invalid.
     */
    public function __construct(int $statusCode, array $headers, ?string $body = null)
    {
        if ($statusCode < 100 || $statusCode >= 600) {
            throw new InvalidArgumentException('Invalid HTTP status code: ' . $statusCode);
        }
        $this->statusCode = $statusCode;
        $this->headers = new HeadersCollection($headers);
        $this->body = $body;
    }
    /**
     * Creates a deep clone of this response.
     *
     * Clones the headers collection to ensure the cloned
     * response is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone headers collection
        $this->headers = clone $this->headers;
    }
    /**
     * Gets the HTTP status code.
     *
     * @since 0.1.0
     *
     * @return int The status code.
     */
    public function getStatusCode(): int
    {
        return $this->statusCode;
    }
    /**
     * Gets the response headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> The headers.
     */
    public function getHeaders(): array
    {
        return $this->headers->getAll();
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function getHeader(string $name): ?array
    {
        return $this->headers->get($name);
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string or null if not found.
     */
    public function getHeaderAsString(string $name): ?string
    {
        return $this->headers->getAsString($name);
    }
    /**
     * Gets the response body.
     *
     * @since 0.1.0
     *
     * @return string|null The body.
     */
    public function getBody(): ?string
    {
        return $this->body;
    }
    /**
     * Checks if the response has a header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @return bool True if the header exists, false otherwise.
     */
    public function hasHeader(string $name): bool
    {
        return $this->headers->has($name);
    }
    /**
     * Checks if the response indicates success.
     *
     * @since 0.1.0
     *
     * @return bool True if status code is 2xx, false otherwise.
     */
    public function isSuccessful(): bool
    {
        return $this->statusCode >= 200 && $this->statusCode < 300;
    }
    /**
     * Gets the response data as an array.
     *
     * Attempts to decode the body as JSON. Returns null if the body
     * is empty or not valid JSON.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The decoded data or null.
     */
    public function getData(): ?array
    {
        if ($this->body === null || $this->body === '') {
            return null;
        }
        $data = json_decode($this->body, \true);
        if (json_last_error() !== \JSON_ERROR_NONE) {
            return null;
        }
        /** @var array<string, mixed>|null $data */
        return is_array($data) ? $data : null;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_STATUS_CODE => ['type' => 'integer', 'minimum' => 100, 'maximum' => 599, 'description' => 'The HTTP status code.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The response headers.'], self::KEY_BODY => ['type' => ['string', 'null'], 'description' => 'The response body.']], 'required' => [self::KEY_STATUS_CODE, self::KEY_HEADERS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ResponseArrayShape
     */
    public function toArray(): array
    {
        $data = [self::KEY_STATUS_CODE => $this->statusCode, self::KEY_HEADERS => $this->headers->getAll()];
        if ($this->body !== null) {
            $data[self::KEY_BODY] = $this->body;
        }
        return $data;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_STATUS_CODE, self::KEY_HEADERS]);
        return new self($array[self::KEY_STATUS_CODE], $array[self::KEY_HEADERS], $array[self::KEY_BODY] ?? null);
    }
}
                                    Providers/Http/DTO/Request.php                                                                      0000644                 00000030047 15227644447 0012252 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\DTO;

use JsonException;
use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\Collections\HeadersCollection;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
/**
 * Represents an HTTP request.
 *
 * This class encapsulates HTTP request data that can be converted
 * to PSR-7 requests by the HTTP transporter.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type RequestOptionsArrayShape from RequestOptions
 * @phpstan-type RequestArrayShape array{
 *     method: string,
 *     uri: string,
 *     headers: array<string, list<string>>,
 *     body?: string|null,
 *     options?: RequestOptionsArrayShape
 * }
 *
 * @extends AbstractDataTransferObject<RequestArrayShape>
 */
class Request extends AbstractDataTransferObject
{
    public const KEY_METHOD = 'method';
    public const KEY_URI = 'uri';
    public const KEY_HEADERS = 'headers';
    public const KEY_BODY = 'body';
    public const KEY_OPTIONS = 'options';
    /**
     * @var HttpMethodEnum The HTTP method.
     */
    protected HttpMethodEnum $method;
    /**
     * @var string The request URI.
     */
    protected string $uri;
    /**
     * @var HeadersCollection The request headers.
     */
    protected HeadersCollection $headers;
    /**
     * @var array<string, mixed>|null The request data (for query params or form data).
     */
    protected ?array $data = null;
    /**
     * @var string|null The request body (raw string content).
     */
    protected ?string $body = null;
    /**
     * @var RequestOptions|null Request transport options.
     */
    protected ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $uri The request URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @param RequestOptions|null $options The request transport options.
     *
     * @throws InvalidArgumentException If the URI is empty.
     */
    public function __construct(HttpMethodEnum $method, string $uri, array $headers = [], $data = null, ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options = null)
    {
        if (empty($uri)) {
            throw new InvalidArgumentException('URI cannot be empty.');
        }
        $this->method = $method;
        $this->uri = $uri;
        $this->headers = new HeadersCollection($headers);
        // Separate data and body based on type
        if (is_string($data)) {
            $this->body = $data;
        } elseif (is_array($data)) {
            $this->data = $data;
        }
        $this->options = $options;
    }
    /**
     * Creates a deep clone of this request.
     *
     * Clones the headers collection and request options to ensure
     * the cloned request is independent of the original.
     * The HTTP method enum is immutable and can be safely shared.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone headers collection
        $this->headers = clone $this->headers;
        // Clone request options if present (contains only primitives)
        if ($this->options !== null) {
            $this->options = clone $this->options;
        }
        // Note: $method is an immutable enum and can be safely shared
    }
    /**
     * Gets the HTTP method.
     *
     * @since 0.1.0
     *
     * @return HttpMethodEnum The HTTP method.
     */
    public function getMethod(): HttpMethodEnum
    {
        return $this->method;
    }
    /**
     * Gets the request URI.
     *
     * For GET requests with array data, appends the data as query parameters.
     *
     * @since 0.1.0
     *
     * @return string The URI.
     */
    public function getUri(): string
    {
        // If GET request with data, append as query parameters
        if ($this->method === HttpMethodEnum::GET() && $this->data !== null && !empty($this->data)) {
            $separator = str_contains($this->uri, '?') ? '&' : '?';
            return $this->uri . $separator . http_build_query($this->data);
        }
        return $this->uri;
    }
    /**
     * Gets the request headers.
     *
     * @since 0.1.0
     *
     * @return array<string, list<string>> The headers.
     */
    public function getHeaders(): array
    {
        return $this->headers->getAll();
    }
    /**
     * Gets a specific header value.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return list<string>|null The header value(s) or null if not found.
     */
    public function getHeader(string $name): ?array
    {
        return $this->headers->get($name);
    }
    /**
     * Gets header values as a comma-separated string.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return string|null The header values as a comma-separated string, or null if not found.
     */
    public function getHeaderAsString(string $name): ?string
    {
        return $this->headers->getAsString($name);
    }
    /**
     * Checks if a header exists.
     *
     * @since 0.1.0
     *
     * @param string $name The header name (case-insensitive).
     * @return bool True if the header exists, false otherwise.
     */
    public function hasHeader(string $name): bool
    {
        return $this->headers->has($name);
    }
    /**
     * Gets the request body.
     *
     * For GET requests, returns null.
     * For POST/PUT/PATCH requests:
     * - If body is set, returns it as-is
     * - If data is set and Content-Type is JSON, returns JSON-encoded data
     * - If data is set and Content-Type is form, returns URL-encoded data
     *
     * @since 0.1.0
     *
     * @return string|null The body.
     * @throws JsonException If the data cannot be encoded to JSON.
     */
    public function getBody(): ?string
    {
        // GET requests don't have a body
        if (!$this->method->hasBody()) {
            return null;
        }
        // If body is set, return it as-is
        if ($this->body !== null) {
            return $this->body;
        }
        // If data is set, encode based on content type
        if ($this->data !== null) {
            $contentType = $this->getContentType();
            // JSON encoding
            if ($contentType !== null && stripos($contentType, 'application/json') !== \false) {
                return json_encode($this->data, \JSON_THROW_ON_ERROR);
            }
            // Default to URL encoding for forms
            return http_build_query($this->data);
        }
        return null;
    }
    /**
     * Gets the Content-Type header value.
     *
     * @since 0.1.0
     *
     * @return string|null The Content-Type header value or null if not set.
     */
    private function getContentType(): ?string
    {
        $values = $this->getHeader('Content-Type');
        return $values !== null ? $values[0] : null;
    }
    /**
     * Returns a new instance with the specified header.
     *
     * @since 0.1.0
     *
     * @param string $name The header name.
     * @param string|list<string> $value The header value(s).
     * @return self A new instance with the header.
     */
    public function withHeader(string $name, $value): self
    {
        $newHeaders = $this->headers->withHeader($name, $value);
        $new = clone $this;
        $new->headers = $newHeaders;
        return $new;
    }
    /**
     * Returns a new instance with the specified data.
     *
     * @since 0.1.0
     *
     * @param string|array<string, mixed> $data The request data.
     * @return self A new instance with the data.
     */
    public function withData($data): self
    {
        $new = clone $this;
        if (is_string($data)) {
            $new->body = $data;
            $new->data = null;
        } elseif (is_array($data)) {
            $new->data = $data;
            $new->body = null;
        } else {
            $new->data = null;
            $new->body = null;
        }
        return $new;
    }
    /**
     * Gets the request data array.
     *
     * @since 0.1.0
     *
     * @return array<string, mixed>|null The request data array.
     */
    public function getData(): ?array
    {
        return $this->data;
    }
    /**
     * Gets the request options.
     *
     * @since 0.2.0
     *
     * @return RequestOptions|null Request transport options when configured.
     */
    public function getOptions(): ?\WordPress\AiClient\Providers\Http\DTO\RequestOptions
    {
        return $this->options;
    }
    /**
     * Returns a new instance with the specified request options.
     *
     * @since 0.2.0
     *
     * @param RequestOptions|null $options The request options to apply.
     * @return self A new instance with the options.
     */
    public function withOptions(?\WordPress\AiClient\Providers\Http\DTO\RequestOptions $options): self
    {
        $new = clone $this;
        $new->options = $options;
        return $new;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_METHOD => ['type' => 'string', 'description' => 'The HTTP method.'], self::KEY_URI => ['type' => 'string', 'description' => 'The request URI.'], self::KEY_HEADERS => ['type' => 'object', 'additionalProperties' => ['type' => 'array', 'items' => ['type' => 'string']], 'description' => 'The request headers.'], self::KEY_BODY => ['type' => ['string'], 'description' => 'The request body.'], self::KEY_OPTIONS => \WordPress\AiClient\Providers\Http\DTO\RequestOptions::getJsonSchema()], 'required' => [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return RequestArrayShape
     */
    public function toArray(): array
    {
        $array = [
            self::KEY_METHOD => $this->method->value,
            self::KEY_URI => $this->getUri(),
            // Include query params if GET with data
            self::KEY_HEADERS => $this->headers->getAll(),
        ];
        // Include body if present (getBody() handles the conversion)
        $body = $this->getBody();
        if ($body !== null) {
            $array[self::KEY_BODY] = $body;
        }
        if ($this->options !== null) {
            $optionsArray = $this->options->toArray();
            if (!empty($optionsArray)) {
                $array[self::KEY_OPTIONS] = $optionsArray;
            }
        }
        return $array;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_METHOD, self::KEY_URI, self::KEY_HEADERS]);
        return new self(HttpMethodEnum::from($array[self::KEY_METHOD]), $array[self::KEY_URI], $array[self::KEY_HEADERS] ?? [], $array[self::KEY_BODY] ?? null, isset($array[self::KEY_OPTIONS]) ? \WordPress\AiClient\Providers\Http\DTO\RequestOptions::fromArray($array[self::KEY_OPTIONS]) : null);
    }
    /**
     * Creates a Request instance from a PSR-7 RequestInterface.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $psrRequest The PSR-7 request to convert.
     * @return self A new Request instance.
     * @throws InvalidArgumentException If the HTTP method is not supported.
     */
    public static function fromPsrRequest(RequestInterface $psrRequest): self
    {
        $method = HttpMethodEnum::from($psrRequest->getMethod());
        $uri = (string) $psrRequest->getUri();
        // Convert PSR-7 headers to array format expected by our constructor
        /** @var array<string, list<string>> $headers */
        $headers = $psrRequest->getHeaders();
        // Get body content
        $body = $psrRequest->getBody()->getContents();
        $bodyOrData = !empty($body) ? $body : null;
        return new self($method, $uri, $headers, $bodyOrData);
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         Providers/Http/Traits/WithRequestAuthenticationTrait.php                                            0000644                 00000002261 15227644447 0017627 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Traits;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
/**
 * Trait for a class that implements WithRequestAuthenticationInterface.
 *
 * @since 0.1.0
 */
trait WithRequestAuthenticationTrait
{
    /**
     * @var RequestAuthenticationInterface|null The request authentication instance.
     */
    private ?RequestAuthenticationInterface $requestAuthentication = null;
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setRequestAuthentication(RequestAuthenticationInterface $requestAuthentication): void
    {
        $this->requestAuthentication = $requestAuthentication;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getRequestAuthentication(): RequestAuthenticationInterface
    {
        if ($this->requestAuthentication === null) {
            throw new RuntimeException('RequestAuthenticationInterface instance not set. ' . 'Make sure you use the AiClient class for all requests.');
        }
        return $this->requestAuthentication;
    }
}
                                                                                                                                                                                                                                                                                                                                               Providers/Http/Traits/WithHttpTransporterTrait.php                                                  0000644                 00000002106 15227644447 0016460 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Traits;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
/**
 * Trait for a class that implements WithHttpTransporterInterface.
 *
 * @since 0.1.0
 */
trait WithHttpTransporterTrait
{
    /**
     * @var HttpTransporterInterface|null The HTTP transporter instance.
     */
    private ?HttpTransporterInterface $httpTransporter = null;
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void
    {
        $this->httpTransporter = $httpTransporter;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function getHttpTransporter(): HttpTransporterInterface
    {
        if ($this->httpTransporter === null) {
            throw new RuntimeException('HttpTransporterInterface instance not set. Make sure you use the AiClient class for all requests.');
        }
        return $this->httpTransporter;
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                          Providers/Http/Exception/RedirectException.php                                                      0000644                 00000003465 15227644447 0015556 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Response;
/**
 * Exception thrown for 3xx HTTP redirect responses.
 *
 * This represents cases where the server indicates that the request
 * should be retried at a different location, but automatic redirect
 * handling was not successful or not enabled.
 *
 * @since 0.2.0
 */
class RedirectException extends RuntimeException
{
    /**
     * Creates a RedirectException from a redirect response.
     *
     * This method extracts redirect information from the response headers
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP redirect response.
     * @return self
     */
    public static function fromRedirectResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 307 => 'Temporary Redirect', 308 => 'Permanent Redirect'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Redirect error (%d): Request needs to be retried at a different location', $statusCode);
        }
        // Try to extract the redirect location from headers
        $locationValues = $response->getHeader('Location');
        if ($locationValues !== null && !empty($locationValues)) {
            $location = $locationValues[0];
            $errorMessage .= ' - Location: ' . $location;
        }
        return new self($errorMessage, $statusCode);
    }
}
                                                                                                                                                                                                           Providers/Http/Exception/ServerException.php                                                        0000644                 00000003425 15227644447 0015257 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Util\ErrorMessageExtractor;
/**
 * Exception thrown for 5xx HTTP server errors.
 *
 * This represents errors where the server failed to fulfill
 * a valid request due to internal server errors.
 *
 * @since 0.2.0
 */
class ServerException extends RuntimeException
{
    /**
     * Creates a ServerException from a server error response.
     *
     * This method extracts error details from common API response formats
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP response that failed.
     * @return self
     */
    public static function fromServerErrorResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [500 => 'Internal Server Error', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 507 => 'Insufficient Storage', 529 => 'Overloaded'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Server error (%d): Request was rejected due to server-side issue', $statusCode);
        }
        // Extract error message from response data using centralized utility
        $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData());
        if ($extractedError !== null) {
            $errorMessage .= ' - ' . $extractedError;
        }
        return new self($errorMessage, $response->getStatusCode());
    }
}
                                                                                                                                                                                                                                           Providers/Http/Exception/error_log                                                                  0000644                 00000011244 15227644447 0013334 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 20:15:55 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php on line 17
[19-Jul-2026 05:17:57 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ResponseException.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ResponseException.php on line 16
[19-Jul-2026 05:18:00 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/NetworkException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/NetworkException.php on line 17
[19-Jul-2026 05:18:16 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php on line 17
[19-Jul-2026 08:18:09 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php on line 17
[19-Jul-2026 08:18:26 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\InvalidArgumentException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ClientException.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ClientException.php on line 18
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ResponseException.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ResponseException.php on line 16
[20-Jul-2026 08:44:35 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/NetworkException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/NetworkException.php on line 17
[20-Jul-2026 08:44:35 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ServerException.php on line 17
[20-Jul-2026 11:44:10 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php on line 17
[20-Jul-2026 11:44:16 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\InvalidArgumentException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ClientException.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/ClientException.php on line 18
[20-Jul-2026 13:36:06 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\Exception\RuntimeException" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Http/Exception/RedirectException.php on line 17
                                                                                                                                                                                                                                                                                                                                                            Providers/Http/Exception/ResponseException.php                                                      0000644                 00000003041 15227644447 0015601 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\RuntimeException;
/**
 * Exception class for HTTP response errors.
 *
 * This is used when response data is unexpected or malformed,
 * typically indicating that a provider changed in ways our code
 * is not aware of or when parsing response data fails.
 *
 * @since 0.1.0
 */
class ResponseException extends RuntimeException
{
    /**
     * Creates a ResponseException for missing expected data.
     *
     * @since 0.2.0
     *
     * @param string $apiName The name of the API/provider.
     * @param string $fieldName The field that was expected but missing.
     * @return self
     */
    public static function fromMissingData(string $apiName, string $fieldName): self
    {
        $message = sprintf('Unexpected %s API response: Missing the "%s" key.', $apiName, $fieldName);
        return new self($message);
    }
    /**
     * Creates a ResponseException from invalid data in an API response.
     *
     * @since 0.2.0
     *
     * @param string $apiName The name of the API service (e.g., 'OpenAI', 'Anthropic').
     * @param string $fieldName The field that was invalid.
     * @param string $message The specific error message describing the invalid data.
     * @return self
     */
    public static function fromInvalidData(string $apiName, string $fieldName, string $message): self
    {
        return new self(sprintf('Unexpected %s API response: Invalid "%s" key: %s', $apiName, $fieldName, $message));
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               Providers/Http/Exception/ClientException.php                                                        0000644                 00000004655 15227644447 0015235 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Util\ErrorMessageExtractor;
/**
 * Exception thrown for 4xx HTTP client errors.
 *
 * This represents errors where the client request was malformed,
 * unauthorized, forbidden, or otherwise invalid.
 *
 * @since 0.2.0
 */
class ClientException extends InvalidArgumentException
{
    /**
     * The request that failed.
     *
     * @var Request|null
     */
    protected ?Request $request = null;
    /**
     * Returns the request that failed as our Request DTO.
     *
     * @since 0.2.0
     *
     * @return Request
     * @throws \RuntimeException If no request is available
     */
    public function getRequest(): Request
    {
        if ($this->request === null) {
            throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.');
        }
        return $this->request;
    }
    /**
     * Creates a ClientException from a client error response (4xx).
     *
     * This method extracts error details from common API response formats
     * and creates an exception with a descriptive message and status code.
     *
     * @since 0.2.0
     *
     * @param Response $response The HTTP response that failed.
     * @return self
     */
    public static function fromClientErrorResponse(Response $response): self
    {
        $statusCode = $response->getStatusCode();
        $statusTexts = [400 => 'Bad Request', 401 => 'Unauthorized', 403 => 'Forbidden', 404 => 'Not Found', 422 => 'Unprocessable Entity', 429 => 'Too Many Requests'];
        if (isset($statusTexts[$statusCode])) {
            $errorMessage = sprintf('%s (%d)', $statusTexts[$statusCode], $statusCode);
        } else {
            $errorMessage = sprintf('Client error (%d): Request was rejected due to client-side issue', $statusCode);
        }
        // Extract error message from response data using centralized utility
        $extractedError = ErrorMessageExtractor::extractFromResponseData($response->getData());
        if ($extractedError !== null) {
            $errorMessage .= ' - ' . $extractedError;
        }
        return new self($errorMessage, $statusCode);
    }
}
                                                                                   Providers/Http/Exception/NetworkException.php                                                       0000644                 00000003512 15227644447 0015437 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Http\Exception;

use WordPress\AiClientDependencies\Psr\Http\Message\RequestInterface;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Http\DTO\Request;
/**
 * Exception thrown for network-related errors.
 *
 * This includes HTTP transport errors, connection failures,
 * timeouts, and other network-related issues.
 *
 * @since 0.2.0
 */
class NetworkException extends RuntimeException
{
    /**
     * The request that failed.
     *
     * @var Request|null
     */
    protected ?Request $request = null;
    /**
     * Returns the request that failed as our Request DTO.
     *
     * @since 0.2.0
     *
     * @return Request
     * @throws \RuntimeException If no request is available
     */
    public function getRequest(): Request
    {
        if ($this->request === null) {
            throw new \RuntimeException('Request object not available. This exception was directly instantiated. ' . 'Use a factory method that provides request context.');
        }
        return $this->request;
    }
    /**
     * Creates a NetworkException from a PSR-18 network exception.
     *
     * @since 0.2.0
     *
     * @param RequestInterface $psrRequest The PSR-7 request that failed.
     * @param \Throwable $networkException The PSR-18 network exception.
     * @return self
     */
    public static function fromPsr18NetworkException(RequestInterface $psrRequest, \Throwable $networkException): self
    {
        $request = Request::fromPsrRequest($psrRequest);
        $message = sprintf('Network error occurred while sending request to %s: %s', $request->getUri(), $networkException->getMessage());
        $exception = new self($message, 0, $networkException);
        $exception->request = $request;
        return $exception;
    }
}
                                                                                                                                                                                      Providers/Enums/ToolTypeEnum.php                                                                    0000644                 00000001365 15227644447 0012751 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for tool types.
 *
 * @since 0.1.0
 *
 * @method static self functionDeclarations() Creates an instance for FUNCTION_DECLARATIONS type.
 * @method static self webSearch() Creates an instance for WEB_SEARCH type.
 * @method bool isFunctionDeclarations() Checks if the type is FUNCTION_DECLARATIONS.
 * @method bool isWebSearch() Checks if the type is WEB_SEARCH.
 */
class ToolTypeEnum extends AbstractEnum
{
    /**
     * Function declarations tool type.
     */
    public const FUNCTION_DECLARATIONS = 'function_declarations';
    /**
     * Web search tool type.
     */
    public const WEB_SEARCH = 'web_search';
}
                                                                                                                                                                                                                                                                           Providers/Enums/ProviderTypeEnum.php                                                                0000644                 00000001673 15227644447 0013630 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\Enums;

use WordPress\AiClient\Common\AbstractEnum;
/**
 * Enum for provider types.
 *
 * @since 0.1.0
 *
 * @method static self cloud() Creates an instance for CLOUD type.
 * @method static self server() Creates an instance for SERVER type.
 * @method static self client() Creates an instance for CLIENT type.
 * @method bool isCloud() Checks if the type is CLOUD.
 * @method bool isServer() Checks if the type is SERVER.
 * @method bool isClient() Checks if the type is CLIENT.
 */
class ProviderTypeEnum extends AbstractEnum
{
    /**
     * Cloud-based AI provider (e.g. models available via external REST APIs).
     */
    public const CLOUD = 'cloud';
    /**
     * Server-side AI provider (e.g. self-hosted models).
     */
    public const SERVER = 'server';
    /**
     * Client-side AI provider (e.g. browser-based models).
     */
    public const CLIENT = 'client';
}
                                                                     Providers/Enums/error_log                                                                           0000644                 00000002640 15227644447 0011546 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 07:42:59 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ToolTypeEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ToolTypeEnum.php on line 17
[19-Jul-2026 14:35:24 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ProviderTypeEnum.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ProviderTypeEnum.php on line 19
[19-Jul-2026 20:30:12 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ToolTypeEnum.php:17
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ToolTypeEnum.php on line 17
[20-Jul-2026 17:54:36 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractEnum" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ProviderTypeEnum.php:19
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/Enums/ProviderTypeEnum.php on line 19
                                                                                                Providers/error_log                                                                                 0000644                 00000002300 15227644447 0010450 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 06:22:21 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ProviderRegistry.php on line 31
[19-Jul-2026 08:18:16 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/AbstractProvider.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/AbstractProvider.php on line 18
[20-Jul-2026 02:12:09 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ProviderRegistry.php on line 31
[20-Jul-2026 11:44:10 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/AbstractProvider.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/AbstractProvider.php on line 18
                                                                                                                                                                                                                                                                                                                                Providers/DTO/ProviderMetadata.php                                                                  0000644                 00000017364 15227644447 0013145 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Enums\ProviderTypeEnum;
use WordPress\AiClient\Providers\Http\Enums\RequestAuthenticationMethod;
/**
 * Represents metadata about an AI provider.
 *
 * This class contains information about an AI provider, including its
 * unique identifier, display name, and type (cloud, server, or client).
 *
 * @since 0.1.0
 * @since 1.2.0 Added optional description property.
 * @since 1.3.0 Added optional logoPath property.
 *
 * @phpstan-type ProviderMetadataArrayShape array{
 *     id: string,
 *     name: string,
 *     description?: ?string,
 *     type: string,
 *     credentialsUrl?: ?string,
 *     authenticationMethod?: ?string,
 *     logoPath?: ?string
 * }
 *
 * @extends AbstractDataTransferObject<ProviderMetadataArrayShape>
 */
class ProviderMetadata extends AbstractDataTransferObject
{
    public const KEY_ID = 'id';
    public const KEY_NAME = 'name';
    public const KEY_DESCRIPTION = 'description';
    public const KEY_TYPE = 'type';
    public const KEY_CREDENTIALS_URL = 'credentialsUrl';
    public const KEY_AUTHENTICATION_METHOD = 'authenticationMethod';
    public const KEY_LOGO_PATH = 'logoPath';
    /**
     * @var string The provider's unique identifier.
     */
    protected string $id;
    /**
     * @var string The provider's display name.
     */
    protected string $name;
    /**
     * @var string|null The provider's description.
     */
    protected ?string $description;
    /**
     * @var ProviderTypeEnum The provider type.
     */
    protected ProviderTypeEnum $type;
    /**
     * @var string|null The URL where users can get credentials.
     */
    protected ?string $credentialsUrl;
    /**
     * @var RequestAuthenticationMethod|null The authentication method.
     */
    protected ?RequestAuthenticationMethod $authenticationMethod;
    /**
     * @var string|null The full path to the provider's logo image file.
     */
    protected ?string $logoPath;
    /**
     * Constructor.
     *
     * @since 0.1.0
     * @since 1.2.0 Added optional $description parameter.
     * @since 1.3.0 Added optional $logoPath parameter.
     *
     * @param string $id The provider's unique identifier.
     * @param string $name The provider's display name.
     * @param ProviderTypeEnum $type The provider type.
     * @param string|null $credentialsUrl The URL where users can get credentials.
     * @param RequestAuthenticationMethod|null $authenticationMethod The authentication method.
     * @param string|null $description The provider's description.
     * @param string|null $logoPath The full path to the provider's logo image file.
     * @throws InvalidArgumentException If the provider ID contains invalid characters.
     */
    public function __construct(string $id, string $name, ProviderTypeEnum $type, ?string $credentialsUrl = null, ?RequestAuthenticationMethod $authenticationMethod = null, ?string $description = null, ?string $logoPath = null)
    {
        if (!preg_match('/^[a-z0-9\-_]+$/', $id)) {
            throw new InvalidArgumentException(sprintf(
                // phpcs:ignore Generic.Files.LineLength.TooLong
                'Invalid provider ID "%s". Only lowercase alphanumeric characters, hyphens, and underscores are allowed.',
                $id
            ));
        }
        $this->id = $id;
        $this->name = $name;
        $this->description = $description;
        $this->type = $type;
        $this->credentialsUrl = $credentialsUrl;
        $this->authenticationMethod = $authenticationMethod;
        $this->logoPath = $logoPath;
    }
    /**
     * Gets the provider's unique identifier.
     *
     * @since 0.1.0
     *
     * @return string The provider ID.
     */
    public function getId(): string
    {
        return $this->id;
    }
    /**
     * Gets the provider's display name.
     *
     * @since 0.1.0
     *
     * @return string The provider name.
     */
    public function getName(): string
    {
        return $this->name;
    }
    /**
     * Gets the provider's description.
     *
     * @since 1.2.0
     *
     * @return string|null The provider description.
     */
    public function getDescription(): ?string
    {
        return $this->description;
    }
    /**
     * Gets the provider type.
     *
     * @since 0.1.0
     *
     * @return ProviderTypeEnum The provider type.
     */
    public function getType(): ProviderTypeEnum
    {
        return $this->type;
    }
    /**
     * Gets the credentials URL.
     *
     * @since 0.1.0
     *
     * @return string|null The credentials URL.
     */
    public function getCredentialsUrl(): ?string
    {
        return $this->credentialsUrl;
    }
    /**
     * Gets the authentication method.
     *
     * @since 0.4.0
     *
     * @return RequestAuthenticationMethod|null The authentication method.
     */
    public function getAuthenticationMethod(): ?RequestAuthenticationMethod
    {
        return $this->authenticationMethod;
    }
    /**
     * Gets the full path to the provider's logo image file.
     *
     * @since 1.3.0
     *
     * @return string|null The full path to the logo image file.
     */
    public function getLogoPath(): ?string
    {
        return $this->logoPath;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description to schema.
     * @since 1.3.0 Added logoPath to schema.
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_ID => ['type' => 'string', 'description' => 'The provider\'s unique identifier.'], self::KEY_NAME => ['type' => 'string', 'description' => 'The provider\'s display name.'], self::KEY_DESCRIPTION => ['type' => 'string', 'description' => 'The provider\'s description.'], self::KEY_TYPE => ['type' => 'string', 'enum' => ProviderTypeEnum::getValues(), 'description' => 'The provider type (cloud, server, or client).'], self::KEY_CREDENTIALS_URL => ['type' => 'string', 'description' => 'The URL where users can get credentials.'], self::KEY_AUTHENTICATION_METHOD => ['type' => ['string', 'null'], 'enum' => array_merge(RequestAuthenticationMethod::getValues(), [null]), 'description' => 'The authentication method.'], self::KEY_LOGO_PATH => ['type' => 'string', 'description' => 'The full path to the provider\'s logo image file.']], 'required' => [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description to output.
     * @since 1.3.0 Added logoPath to output.
     *
     * @return ProviderMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_ID => $this->id, self::KEY_NAME => $this->name, self::KEY_DESCRIPTION => $this->description, self::KEY_TYPE => $this->type->value, self::KEY_CREDENTIALS_URL => $this->credentialsUrl, self::KEY_AUTHENTICATION_METHOD => $this->authenticationMethod ? $this->authenticationMethod->value : null, self::KEY_LOGO_PATH => $this->logoPath];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     * @since 1.2.0 Added description support.
     * @since 1.3.0 Added logoPath support.
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_ID, self::KEY_NAME, self::KEY_TYPE]);
        return new self($array[self::KEY_ID], $array[self::KEY_NAME], ProviderTypeEnum::from($array[self::KEY_TYPE]), $array[self::KEY_CREDENTIALS_URL] ?? null, isset($array[self::KEY_AUTHENTICATION_METHOD]) ? RequestAuthenticationMethod::from($array[self::KEY_AUTHENTICATION_METHOD]) : null, $array[self::KEY_DESCRIPTION] ?? null, $array[self::KEY_LOGO_PATH] ?? null);
    }
}
                                                                                                                                                                                                                                                                            Providers/DTO/error_log                                                                             0000644                 00000003546 15227644447 0011113 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [17-Jul-2026 20:15:38 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php:32
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php on line 32
[19-Jul-2026 05:17:58 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderModelsMetadata.php:27
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderModelsMetadata.php on line 27
[19-Jul-2026 08:18:08 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php:32
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php on line 32
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderModelsMetadata.php:27
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderModelsMetadata.php on line 27
[20-Jul-2026 11:44:09 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Common\AbstractDataTransferObject" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php:32
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php on line 32
                                                                                                                                                          Providers/DTO/ProviderModelsMetadata.php                                                            0000644                 00000010144 15227644447 0014276 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\DTO;

use WordPress\AiClient\Common\AbstractDataTransferObject;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Represents metadata about a provider and its available models.
 *
 * This class combines provider information with the models that
 * the provider offers, facilitating model discovery and selection.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type ProviderMetadataArrayShape from ProviderMetadata
 * @phpstan-import-type ModelMetadataArrayShape from ModelMetadata
 *
 * @phpstan-type ProviderModelsMetadataArrayShape array{
 *     provider: ProviderMetadataArrayShape,
 *     models: list<ModelMetadataArrayShape>
 * }
 *
 * @extends AbstractDataTransferObject<ProviderModelsMetadataArrayShape>
 */
class ProviderModelsMetadata extends AbstractDataTransferObject
{
    public const KEY_PROVIDER = 'provider';
    public const KEY_MODELS = 'models';
    /**
     * @var ProviderMetadata The provider metadata.
     */
    protected \WordPress\AiClient\Providers\DTO\ProviderMetadata $provider;
    /**
     * @var list<ModelMetadata> The available models.
     */
    protected array $models;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ProviderMetadata $provider The provider metadata.
     * @param list<ModelMetadata> $models The available models.
     *
     * @throws InvalidArgumentException If models is not a list.
     */
    public function __construct(\WordPress\AiClient\Providers\DTO\ProviderMetadata $provider, array $models)
    {
        if (!array_is_list($models)) {
            throw new InvalidArgumentException('Models must be a list array.');
        }
        $this->provider = $provider;
        $this->models = $models;
    }
    /**
     * Creates a deep clone of this metadata.
     *
     * Clones the provider metadata and all model metadata objects
     * to ensure the cloned instance is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Clone provider metadata
        $this->provider = clone $this->provider;
        // Deep clone models array (ModelMetadata has __clone)
        $clonedModels = [];
        foreach ($this->models as $model) {
            $clonedModels[] = clone $model;
        }
        $this->models = $clonedModels;
    }
    /**
     * Gets the provider metadata.
     *
     * @since 0.1.0
     *
     * @return ProviderMetadata The provider metadata.
     */
    public function getProvider(): \WordPress\AiClient\Providers\DTO\ProviderMetadata
    {
        return $this->provider;
    }
    /**
     * Gets the available models.
     *
     * @since 0.1.0
     *
     * @return list<ModelMetadata> The available models.
     */
    public function getModels(): array
    {
        return $this->models;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function getJsonSchema(): array
    {
        return ['type' => 'object', 'properties' => [self::KEY_PROVIDER => \WordPress\AiClient\Providers\DTO\ProviderMetadata::getJsonSchema(), self::KEY_MODELS => ['type' => 'array', 'items' => ModelMetadata::getJsonSchema(), 'description' => 'The available models for this provider.']], 'required' => [self::KEY_PROVIDER, self::KEY_MODELS]];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     *
     * @return ProviderModelsMetadataArrayShape
     */
    public function toArray(): array
    {
        return [self::KEY_PROVIDER => $this->provider->toArray(), self::KEY_MODELS => array_map(static fn(ModelMetadata $model): array => $model->toArray(), $this->models)];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public static function fromArray(array $array): self
    {
        static::validateFromArrayData($array, [self::KEY_PROVIDER, self::KEY_MODELS]);
        return new self(\WordPress\AiClient\Providers\DTO\ProviderMetadata::fromArray($array[self::KEY_PROVIDER]), array_map(static fn(array $modelData): ModelMetadata => ModelMetadata::fromArray($modelData), $array[self::KEY_MODELS]));
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                            Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php            0000644                 00000061237 15227644447 0026044 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessagePartChannelEnum;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Results\DTO\Candidate;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Results\DTO\TokenUsage;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
/**
 * Base class for a text generation model for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * API endpoint, including but not limited to Anthropic, Google, and other providers
 * that have adopted OpenAI's API specification as a standard interface.
 *
 * @since 0.1.0
 *
 * @phpstan-type ToolCallData array{
 *     type?: string,
 *     id?: string,
 *     function?: array{
 *         name?: string,
 *         arguments: string|array<string, mixed>
 *     }
 * }
 * @phpstan-type MessageData array{
 *     role?: string,
 *     reasoning_content?: string,
 *     content?: string,
 *     tool_calls?: list<ToolCallData>
 * }
 * @phpstan-type ChoiceData array{
 *     message?: MessageData,
 *     finish_reason?: string
 * }
 * @phpstan-type UsageData array{
 *     prompt_tokens?: int,
 *     completion_tokens?: int,
 *     total_tokens?: int
 * }
 * @phpstan-type ResponseData array{
 *     id?: string,
 *     choices?: list<ChoiceData>,
 *     usage?: UsageData
 * }
 */
abstract class AbstractOpenAiCompatibleTextGenerationModel extends AbstractApiBasedModel implements TextGenerationModelInterface
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function generateTextResult(array $prompt): GenerativeAiResult
    {
        $httpTransporter = $this->getHttpTransporter();
        $params = $this->prepareGenerateTextParams($prompt);
        $request = $this->createRequest(HttpMethodEnum::POST(), 'chat/completions', ['Content-Type' => 'application/json'], $params);
        // Add authentication credentials to the request.
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        // Send and process the request.
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        return $this->parseResponseToGenerativeAiResult($response);
    }
    /**
     * Prepares the given prompt and the model configuration into parameters for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt The prompt to generate text for. Either a single message or a list of messages
     *                              from a chat.
     * @return array<string, mixed> The parameters for the API request.
     */
    protected function prepareGenerateTextParams(array $prompt): array
    {
        $config = $this->getConfig();
        $params = ['model' => $this->metadata()->getId(), 'messages' => $this->prepareMessagesParam($prompt, $config->getSystemInstruction())];
        $outputModalities = $config->getOutputModalities();
        if (is_array($outputModalities)) {
            $this->validateOutputModalities($outputModalities);
            if (count($outputModalities) > 1) {
                $params['modalities'] = $this->prepareOutputModalitiesParam($outputModalities);
            }
        }
        $candidateCount = $config->getCandidateCount();
        if ($candidateCount !== null) {
            $params['n'] = $candidateCount;
        }
        $maxTokens = $config->getMaxTokens();
        if ($maxTokens !== null) {
            $params['max_tokens'] = $maxTokens;
        }
        $temperature = $config->getTemperature();
        if ($temperature !== null) {
            $params['temperature'] = $temperature;
        }
        $topP = $config->getTopP();
        if ($topP !== null) {
            $params['top_p'] = $topP;
        }
        $stopSequences = $config->getStopSequences();
        if (is_array($stopSequences)) {
            $params['stop'] = $stopSequences;
        }
        $presencePenalty = $config->getPresencePenalty();
        if ($presencePenalty !== null) {
            $params['presence_penalty'] = $presencePenalty;
        }
        $frequencyPenalty = $config->getFrequencyPenalty();
        if ($frequencyPenalty !== null) {
            $params['frequency_penalty'] = $frequencyPenalty;
        }
        $logprobs = $config->getLogprobs();
        if ($logprobs !== null) {
            $params['logprobs'] = $logprobs;
        }
        $topLogprobs = $config->getTopLogprobs();
        if ($topLogprobs !== null) {
            $params['top_logprobs'] = $topLogprobs;
        }
        $functionDeclarations = $config->getFunctionDeclarations();
        if (is_array($functionDeclarations)) {
            $params['tools'] = $this->prepareToolsParam($functionDeclarations);
        }
        $outputMimeType = $config->getOutputMimeType();
        if ('application/json' === $outputMimeType) {
            $outputSchema = $config->getOutputSchema();
            $params['response_format'] = $this->prepareResponseFormatParam($outputSchema);
        }
        /*
         * Any custom options are added to the parameters as well.
         * This allows developers to pass other options that may be more niche or not yet supported by the SDK.
         */
        $customOptions = $config->getCustomOptions();
        foreach ($customOptions as $key => $value) {
            if (isset($params[$key])) {
                throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key));
            }
            $params[$key] = $value;
        }
        return $params;
    }
    /**
     * Prepares the messages parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $messages The messages to prepare.
     * @param string|null $systemInstruction An optional system instruction to prepend to the messages.
     * @return list<array<string, mixed>> The prepared messages parameter.
     */
    protected function prepareMessagesParam(array $messages, ?string $systemInstruction = null): array
    {
        $messagesParam = array_map(function (Message $message): array {
            // Special case: Function response.
            $messageParts = $message->getParts();
            if (count($messageParts) === 1 && $messageParts[0]->getType()->isFunctionResponse()) {
                $functionResponse = $messageParts[0]->getFunctionResponse();
                if (!$functionResponse) {
                    // This should be impossible due to class internals, but still needs to be checked.
                    throw new RuntimeException('The function response typed message part must contain a function response.');
                }
                return ['role' => 'tool', 'content' => json_encode($functionResponse->getResponse()), 'tool_call_id' => $functionResponse->getId()];
            }
            $messageData = ['role' => $this->getMessageRoleString($message->getRole()), 'content' => array_values(array_filter(array_map([$this, 'getMessagePartContentData'], $messageParts)))];
            // Only include tool_calls if there are any (OpenAI rejects empty arrays).
            $toolCalls = array_values(array_filter(array_map([$this, 'getMessagePartToolCallData'], $messageParts)));
            if (!empty($toolCalls)) {
                $messageData['tool_calls'] = $toolCalls;
            }
            return $messageData;
        }, $messages);
        if ($systemInstruction) {
            array_unshift($messagesParam, [
                /*
                 * TODO: Replace this with 'developer' in the future.
                 * See https://platform.openai.com/docs/api-reference/chat/create#chat_create-messages
                 */
                'role' => 'system',
                'content' => [['type' => 'text', 'text' => $systemInstruction]],
            ]);
        }
        return $messagesParam;
    }
    /**
     * Returns the OpenAI API specific role string for the given message role.
     *
     * @since 0.1.0
     *
     * @param MessageRoleEnum $role The message role.
     * @return string The role for the API request.
     */
    protected function getMessageRoleString(MessageRoleEnum $role): string
    {
        if ($role === MessageRoleEnum::model()) {
            return 'assistant';
        }
        return 'user';
    }
    /**
     * Returns the OpenAI API specific content data for a message part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The message part to get the data for.
     * @return ?array<string, mixed> The data for the message content part, or null if not applicable.
     * @throws InvalidArgumentException If the message part type or data is unsupported.
     */
    protected function getMessagePartContentData(MessagePart $part): ?array
    {
        $type = $part->getType();
        if ($type->isText()) {
            /*
             * The OpenAI Chat Completions API spec does not support annotating thought parts as input,
             * so we instead skip them.
             */
            if ($part->getChannel()->isThought()) {
                return null;
            }
            return ['type' => 'text', 'text' => $part->getText()];
        }
        if ($type->isFile()) {
            $file = $part->getFile();
            if (!$file) {
                // This should be impossible due to class internals, but still needs to be checked.
                throw new RuntimeException('The file typed message part must contain a file.');
            }
            if ($file->isRemote()) {
                if ($file->isImage()) {
                    return ['type' => 'image_url', 'image_url' => ['url' => $file->getUrl()]];
                }
                throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for remote file message part.', $file->getMimeType()));
            }
            // Else, it is an inline file.
            if ($file->isImage()) {
                return ['type' => 'image_url', 'image_url' => ['url' => $file->getDataUri()]];
            }
            if ($file->isAudio()) {
                return ['type' => 'input_audio', 'input_audio' => ['data' => $file->getBase64Data(), 'format' => $file->getMimeTypeObject()->toExtension()]];
            }
            throw new InvalidArgumentException(sprintf('Unsupported MIME type "%s" for inline file message part.', $file->getMimeType()));
        }
        if ($type->isFunctionCall()) {
            // Skip, as this is separately included. See `getMessagePartToolCallData()`.
            return null;
        }
        if ($type->isFunctionResponse()) {
            // Special case: Function response.
            throw new InvalidArgumentException('The API only allows a single function response, as the only content of the message.');
        }
        throw new InvalidArgumentException(sprintf('Unsupported message part type "%s".', $type));
    }
    /**
     * Returns the OpenAI API specific tool calls data for a message part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The message part to get the data for.
     * @return ?array<string, mixed> The data for the message tool call part, or null if not applicable.
     * @throws InvalidArgumentException If the message part type or data is unsupported.
     */
    protected function getMessagePartToolCallData(MessagePart $part): ?array
    {
        $type = $part->getType();
        if ($type->isFunctionCall()) {
            $functionCall = $part->getFunctionCall();
            if (!$functionCall) {
                // This should be impossible due to class internals, but still needs to be checked.
                throw new RuntimeException('The function call typed message part must contain a function call.');
            }
            $args = $functionCall->getArgs();
            /*
             * Ensure null or empty arrays become empty objects for JSON encoding.
             * While in theory the JSON schema could also dictate a type of
             * 'array', in practice function arguments are typically of type
             * 'object'. More importantly, the OpenAI API specification seems
             * to expect that, and does not support passing arrays as the root
             * value. The null check handles the case where FunctionCall normalizes
             * empty arrays to null.
             */
            if ($args === null || is_array($args) && count($args) === 0) {
                $args = new \stdClass();
            }
            return ['type' => 'function', 'id' => $functionCall->getId(), 'function' => ['name' => $functionCall->getName(), 'arguments' => json_encode($args)]];
        }
        // All other types are handled in `getMessagePartContentData()`.
        return null;
    }
    /**
     * Validates that the given output modalities to ensure that at least one output modality is text.
     *
     * @since 0.1.0
     *
     * @param array<ModalityEnum> $outputModalities The output modalities to validate.
     * @throws InvalidArgumentException If no text output modality is present.
     */
    protected function validateOutputModalities(array $outputModalities): void
    {
        // If no output modalities are set, it's fine, as we can assume text.
        if (count($outputModalities) === 0) {
            return;
        }
        foreach ($outputModalities as $modality) {
            if ($modality->isText()) {
                return;
            }
        }
        throw new InvalidArgumentException('A text output modality must be present when generating text.');
    }
    /**
     * Prepares the output modalities parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param array<ModalityEnum> $modalities The modalities to prepare.
     * @return list<string> The prepared modalities parameter.
     */
    protected function prepareOutputModalitiesParam(array $modalities): array
    {
        $prepared = [];
        foreach ($modalities as $modality) {
            if ($modality->isText()) {
                $prepared[] = 'text';
            } elseif ($modality->isImage()) {
                $prepared[] = 'image';
            } elseif ($modality->isAudio()) {
                $prepared[] = 'audio';
            } else {
                throw new InvalidArgumentException(sprintf('Unsupported output modality "%s".', $modality));
            }
        }
        return $prepared;
    }
    /**
     * Prepares the tools parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<FunctionDeclaration> $functionDeclarations The function declarations.
     * @return list<array<string, mixed>> The prepared tools parameter.
     */
    protected function prepareToolsParam(array $functionDeclarations): array
    {
        $tools = [];
        foreach ($functionDeclarations as $functionDeclaration) {
            $tools[] = ['type' => 'function', 'function' => $functionDeclaration->toArray()];
        }
        return $tools;
    }
    /**
     * Prepares the response format parameter for the API request.
     *
     * This is only called if the output MIME type is `application/json`.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed>|null $outputSchema The output schema.
     * @return array<string, mixed> The prepared response format parameter.
     */
    protected function prepareResponseFormatParam(?array $outputSchema): array
    {
        if (is_array($outputSchema)) {
            return ['type' => 'json_schema', 'json_schema' => $outputSchema];
        }
        return ['type' => 'json_object'];
    }
    /**
     * Creates a request object for the provider's API.
     *
     * Implementations should use $this->getRequestOptions() to attach any
     * configured request options to the Request.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to a generative AI result.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint.
     * @return GenerativeAiResult The parsed generative AI result.
     */
    protected function parseResponseToGenerativeAiResult(Response $response): GenerativeAiResult
    {
        /** @var ResponseData $responseData */
        $responseData = $response->getData();
        if (!isset($responseData['choices']) || !$responseData['choices']) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'choices');
        }
        if (!is_array($responseData['choices'])) {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'choices', 'The value must be an array.');
        }
        $candidates = [];
        foreach ($responseData['choices'] as $index => $choiceData) {
            if (!is_array($choiceData) || array_is_list($choiceData)) {
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must be an associative array.');
            }
            $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index);
        }
        $id = isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : '';
        if (isset($responseData['usage']) && is_array($responseData['usage'])) {
            $usage = $responseData['usage'];
            $tokenUsage = new TokenUsage($usage['prompt_tokens'] ?? 0, $usage['completion_tokens'] ?? 0, $usage['total_tokens'] ?? 0);
        } else {
            $tokenUsage = new TokenUsage(0, 0, 0);
        }
        // Use any other data from the response as provider-specific response metadata.
        $additionalData = $responseData;
        unset($additionalData['id'], $additionalData['choices'], $additionalData['usage']);
        return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $additionalData);
    }
    /**
     * Parses a single choice from the API response into a Candidate object.
     *
     * @since 0.1.0
     *
     * @param ChoiceData $choiceData The choice data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return Candidate The parsed candidate.
     * @throws RuntimeException If the choice data is invalid.
     */
    protected function parseResponseChoiceToCandidate(array $choiceData, int $index): Candidate
    {
        if (!isset($choiceData['message']) || !is_array($choiceData['message']) || array_is_list($choiceData['message'])) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].message");
        }
        if (!isset($choiceData['finish_reason']) || !is_string($choiceData['finish_reason'])) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason");
        }
        $messageData = $choiceData['message'];
        $message = $this->parseResponseChoiceMessage($messageData, $index);
        switch ($choiceData['finish_reason']) {
            case 'stop':
                $finishReason = FinishReasonEnum::stop();
                break;
            case 'length':
                $finishReason = FinishReasonEnum::length();
                break;
            case 'content_filter':
                $finishReason = FinishReasonEnum::contentFilter();
                break;
            case 'tool_calls':
                $finishReason = FinishReasonEnum::toolCalls();
                break;
            default:
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].finish_reason", sprintf('Invalid finish reason "%s".', $choiceData['finish_reason']));
        }
        return new Candidate($message, $finishReason);
    }
    /**
     * Parses the message from a choice in the API response.
     *
     * @since 0.1.0
     *
     * @param MessageData $messageData The message data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return Message The parsed message.
     */
    protected function parseResponseChoiceMessage(array $messageData, int $index): Message
    {
        $role = isset($messageData['role']) && 'user' === $messageData['role'] ? MessageRoleEnum::user() : MessageRoleEnum::model();
        $parts = $this->parseResponseChoiceMessageParts($messageData, $index);
        return new Message($role, $parts);
    }
    /**
     * Parses the message parts from a choice in the API response.
     *
     * @since 0.1.0
     *
     * @param MessageData $messageData The message data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @return MessagePart[] The parsed message parts.
     */
    protected function parseResponseChoiceMessageParts(array $messageData, int $index): array
    {
        $parts = [];
        if (isset($messageData['reasoning_content']) && is_string($messageData['reasoning_content'])) {
            $parts[] = new MessagePart($messageData['reasoning_content'], MessagePartChannelEnum::thought());
        }
        if (isset($messageData['content']) && is_string($messageData['content'])) {
            $parts[] = new MessagePart($messageData['content']);
        }
        if (isset($messageData['tool_calls']) && is_array($messageData['tool_calls'])) {
            foreach ($messageData['tool_calls'] as $toolCallIndex => $toolCallData) {
                $toolCallPart = $this->parseResponseChoiceMessageToolCallPart($toolCallData);
                if (!$toolCallPart) {
                    throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}].message.tool_calls[{$toolCallIndex}]", 'The response includes a tool call of an unexpected type.');
                }
                $parts[] = $toolCallPart;
            }
        }
        return $parts;
    }
    /**
     * Parses a tool call part from the API response.
     *
     * @since 0.1.0
     *
     * @param ToolCallData $toolCallData The tool call data from the API response.
     * @return MessagePart|null The parsed message part for the tool call, or null if not applicable.
     */
    protected function parseResponseChoiceMessageToolCallPart(array $toolCallData): ?MessagePart
    {
        /*
         * For now, only function calls are supported.
         *
         * Not all OpenAI compatible APIs include a 'type' key, so we only check its value if it is set.
         */
        if (isset($toolCallData['type']) && 'function' !== $toolCallData['type'] || !isset($toolCallData['function']) || !is_array($toolCallData['function'])) {
            return null;
        }
        $functionArguments = is_string($toolCallData['function']['arguments']) ? json_decode($toolCallData['function']['arguments'], \true) : $toolCallData['function']['arguments'];
        $functionCall = new FunctionCall(isset($toolCallData['id']) && is_string($toolCallData['id']) ? $toolCallData['id'] : null, isset($toolCallData['function']['name']) && is_string($toolCallData['function']['name']) ? $toolCallData['function']['name'] : null, $functionArguments);
        return new MessagePart($functionCall);
    }
}
                                                                                                                                                                                                                                                                                                                                                                 Providers/OpenAiCompatibleImplementation/error_log                                                  0000644                 00000005774 15227644447 0016553 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [19-Jul-2026 08:18:11 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModelMetadataDirectory" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php:22
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php on line 22
[19-Jul-2026 14:35:38 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php:57
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php on line 57
[19-Jul-2026 14:35:41 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php:64
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php on line 64
[20-Jul-2026 11:44:11 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModelMetadataDirectory" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php:22
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php on line 22
[20-Jul-2026 17:54:38 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php:57
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php on line 57
[20-Jul-2026 17:54:38 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php:64
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php on line 64
    Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php           0000644                 00000031733 15227644447 0026140 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModel;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
use WordPress\AiClient\Results\DTO\Candidate;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Results\DTO\TokenUsage;
use WordPress\AiClient\Results\Enums\FinishReasonEnum;
/**
 * Base class for an image generation model for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * API endpoint for image generation, including but not limited to Anthropic, Google, and other
 * providers that have adopted OpenAI's image generation API specification as a standard interface.
 *
 * @since 0.1.0
 *
 * @phpstan-type ImageGenerationParams array{
 *     model: string,
 *     prompt: string,
 *     n?: int,
 *     response_format?: string,
 *     output_format?: string|null,
 *     size?: string,
 *     ...
 * }
 * @phpstan-type ChoiceData array{
 *     url?: string,
 *     b64_json?: string
 * }
 * @phpstan-type UsageData array{
 *     input_tokens?: int,
 *     output_tokens?: int,
 *     total_tokens?: int
 * }
 * @phpstan-type ResponseData array{
 *     id?: string,
 *     data?: list<ChoiceData>,
 *     usage?: UsageData
 * }
 */
abstract class AbstractOpenAiCompatibleImageGenerationModel extends AbstractApiBasedModel implements ImageGenerationModelInterface
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function generateImageResult(array $prompt): GenerativeAiResult
    {
        $httpTransporter = $this->getHttpTransporter();
        $params = $this->prepareGenerateImageParams($prompt);
        $request = $this->createRequest(HttpMethodEnum::POST(), 'images/generations', ['Content-Type' => 'application/json'], $params);
        // Add authentication credentials to the request.
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        // Send and process the request.
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        return $this->parseResponseToGenerativeAiResult($response, isset($params['output_format']) && is_string($params['output_format']) ? "image/{$params['output_format']}" : 'image/png');
    }
    /**
     * Prepares the given prompt and the model configuration into parameters for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $prompt The prompt to generate an image for. Either a single message or a list of messages
     *                              from a chat. However as of today, OpenAI compatible image generation endpoints only
     *                              support a single user message.
     * @return ImageGenerationParams The parameters for the API request.
     */
    protected function prepareGenerateImageParams(array $prompt): array
    {
        $config = $this->getConfig();
        $params = ['model' => $this->metadata()->getId(), 'prompt' => $this->preparePromptParam($prompt)];
        $candidateCount = $config->getCandidateCount();
        if ($candidateCount !== null) {
            $params['n'] = $candidateCount;
        }
        $outputFileType = $config->getOutputFileType();
        if ($outputFileType !== null) {
            $params['response_format'] = $outputFileType->isRemote() ? 'url' : 'b64_json';
        } else {
            // The 'response_format' parameter is required, so we default to 'b64_json' if not set.
            $params['response_format'] = 'b64_json';
        }
        $outputMimeType = $config->getOutputMimeType();
        if ($outputMimeType !== null) {
            $params['output_format'] = preg_replace('/^image\//', '', $outputMimeType);
        }
        $outputMediaOrientation = $config->getOutputMediaOrientation();
        $outputMediaAspectRatio = $config->getOutputMediaAspectRatio();
        if ($outputMediaOrientation !== null || $outputMediaAspectRatio !== null) {
            $params['size'] = $this->prepareSizeParam($outputMediaOrientation, $outputMediaAspectRatio);
        }
        /*
         * Any custom options are added to the parameters as well.
         * This allows developers to pass other options that may be more niche or not yet supported by the SDK.
         */
        $customOptions = $config->getCustomOptions();
        foreach ($customOptions as $key => $value) {
            if (isset($params[$key])) {
                throw new InvalidArgumentException(sprintf('The custom option "%s" conflicts with an existing parameter.', $key));
            }
            $params[$key] = $value;
        }
        /** @var ImageGenerationParams $params */
        return $params;
    }
    /**
     * Prepares the prompt parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param list<Message> $messages The messages to prepare. However as of today, OpenAI compatible image generation
     *                                endpoints only support a single user message.
     * @return string The prepared prompt parameter.
     */
    protected function preparePromptParam(array $messages): string
    {
        if (count($messages) !== 1) {
            throw new InvalidArgumentException('The API requires a single user message as prompt.');
        }
        $message = $messages[0];
        if (!$message->getRole()->isUser()) {
            throw new InvalidArgumentException('The API requires a user message as prompt.');
        }
        $text = null;
        foreach ($message->getParts() as $part) {
            $text = $part->getText();
            if ($text !== null) {
                break;
            }
        }
        if ($text === null) {
            throw new InvalidArgumentException('The API requires a single text message part as prompt.');
        }
        return $text;
    }
    /**
     * Prepares the size parameter for the API request.
     *
     * @since 0.1.0
     *
     * @param MediaOrientationEnum|null $orientation The desired media orientation.
     * @param string|null $aspectRatio The desired media aspect ratio.
     * @return string The prepared size parameter.
     */
    protected function prepareSizeParam(?MediaOrientationEnum $orientation, ?string $aspectRatio): string
    {
        // Use aspect ratio if set, as it is more specific.
        if ($aspectRatio !== null) {
            switch ($aspectRatio) {
                case '1:1':
                    return '1024x1024';
                case '3:2':
                    return '1536x1024';
                case '7:4':
                    return '1792x1024';
                case '2:3':
                    return '1024x1536';
                case '4:7':
                    return '1024x1792';
                default:
                    throw new InvalidArgumentException('The aspect ratio "' . $aspectRatio . '" is not supported.');
            }
        }
        // This should always have a value, as the method is only called if at least one or the other is set.
        if ($orientation !== null) {
            if ($orientation->isLandscape()) {
                return '1536x1024';
            }
            if ($orientation->isPortrait()) {
                return '1024x1536';
            }
        }
        return '1024x1024';
    }
    /**
     * Creates a request object for the provider's API.
     *
     * Implementations should use $this->getRequestOptions() to attach any
     * configured request options to the Request.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to a generative AI result.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint.
     * @param string   $expectedMimeType The expected MIME type the response is in.
     * @return GenerativeAiResult The parsed generative AI result.
     */
    protected function parseResponseToGenerativeAiResult(Response $response, string $expectedMimeType = 'image/png'): GenerativeAiResult
    {
        /** @var ResponseData $responseData */
        $responseData = $response->getData();
        if (!isset($responseData['data']) || !$responseData['data']) {
            throw ResponseException::fromMissingData($this->providerMetadata()->getName(), 'data');
        }
        if (!is_array($responseData['data'])) {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), 'data', 'The value must be an array.');
        }
        $candidates = [];
        foreach ($responseData['data'] as $index => $choiceData) {
            if (!is_array($choiceData) || array_is_list($choiceData)) {
                throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "data[{$index}]", 'The value must be an associative array.');
            }
            $candidates[] = $this->parseResponseChoiceToCandidate($choiceData, $index, $expectedMimeType);
        }
        $id = $this->getResultId($responseData);
        if (isset($responseData['usage']) && is_array($responseData['usage'])) {
            $usage = $responseData['usage'];
            $tokenUsage = new TokenUsage($usage['input_tokens'] ?? 0, $usage['output_tokens'] ?? 0, $usage['total_tokens'] ?? 0);
        } else {
            $tokenUsage = new TokenUsage(0, 0, 0);
        }
        // Use any other data from the response as provider-specific response metadata.
        $providerMetadata = $responseData;
        unset($providerMetadata['id'], $providerMetadata['data'], $providerMetadata['usage']);
        return new GenerativeAiResult($id, $candidates, $tokenUsage, $this->providerMetadata(), $this->metadata(), $providerMetadata);
    }
    /**
     * Parses a single choice from the API response into a Candidate object.
     *
     * @since 0.1.0
     *
     * @param ChoiceData $choiceData The choice data from the API response.
     * @param int $index The index of the choice in the choices array.
     * @param string   $expectedMimeType The expected MIME type the response is in.
     * @return Candidate The parsed candidate.
     * @throws RuntimeException If the choice data is invalid.
     */
    protected function parseResponseChoiceToCandidate(array $choiceData, int $index, string $expectedMimeType = 'image/png'): Candidate
    {
        if (isset($choiceData['url']) && is_string($choiceData['url'])) {
            $imageFile = new File($choiceData['url'], $expectedMimeType);
        } elseif (isset($choiceData['b64_json']) && is_string($choiceData['b64_json'])) {
            $imageFile = new File($choiceData['b64_json'], $expectedMimeType);
        } else {
            throw ResponseException::fromInvalidData($this->providerMetadata()->getName(), "choices[{$index}]", 'The value must contain either a url or b64_json key with a string value.');
        }
        $parts = [new MessagePart($imageFile)];
        $message = new Message(MessageRoleEnum::model(), $parts);
        return new Candidate($message, FinishReasonEnum::stop());
    }
    /**
     * Extracts the result ID from the API response data.
     *
     * @since 0.4.0
     *
     * @param array<string, mixed> $responseData The response data from the API.
     * @return string The result ID.
     */
    protected function getResultId(array $responseData): string
    {
        return isset($responseData['id']) && is_string($responseData['id']) ? $responseData['id'] : '';
    }
}
                                     Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleModelMetadataDirectory.php         0000644                 00000006373 15227644447 0026511 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\OpenAiCompatibleImplementation;

use WordPress\AiClient\Providers\ApiBasedImplementation\AbstractApiBasedModelMetadataDirectory;
use WordPress\AiClient\Providers\Http\DTO\Request;
use WordPress\AiClient\Providers\Http\DTO\Response;
use WordPress\AiClient\Providers\Http\Enums\HttpMethodEnum;
use WordPress\AiClient\Providers\Http\Exception\ResponseException;
use WordPress\AiClient\Providers\Http\Util\ResponseUtil;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for a model metadata directory for providers that implement OpenAI's API format.
 *
 * This abstract class is designed to work with any AI provider that offers an OpenAI-compatible
 * models listing endpoint, including but not limited to Anthropic, Google, and other
 * providers that have adopted OpenAI's models API specification as a standard interface.
 *
 * @since 0.1.0
 */
abstract class AbstractOpenAiCompatibleModelMetadataDirectory extends AbstractApiBasedModelMetadataDirectory
{
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    protected function sendListModelsRequest(): array
    {
        $httpTransporter = $this->getHttpTransporter();
        $request = $this->createRequest(HttpMethodEnum::GET(), 'models');
        $request = $this->getRequestAuthentication()->authenticateRequest($request);
        $response = $httpTransporter->send($request);
        $this->throwIfNotSuccessful($response);
        $modelsMetadataList = $this->parseResponseToModelMetadataList($response);
        $modelMetadataMap = [];
        foreach ($modelsMetadataList as $modelMetadata) {
            $modelMetadataMap[$modelMetadata->getId()] = $modelMetadata;
        }
        return $modelMetadataMap;
    }
    /**
     * Creates a request object for the provider's API.
     *
     * @since 0.1.0
     *
     * @param HttpMethodEnum $method The HTTP method.
     * @param string $path The API endpoint path, relative to the base URI.
     * @param array<string, string|list<string>> $headers The request headers.
     * @param string|array<string, mixed>|null $data The request data.
     * @return Request The request object.
     */
    abstract protected function createRequest(HttpMethodEnum $method, string $path, array $headers = [], $data = null): Request;
    /**
     * Throws an exception if the response is not successful.
     *
     * @since 0.1.0
     *
     * @param Response $response The HTTP response to check.
     * @throws ResponseException If the response is not successful.
     */
    protected function throwIfNotSuccessful(Response $response): void
    {
        /*
         * While this method only calls the utility method, it's important to have it here as a protected method so
         * that child classes can override it if needed.
         */
        ResponseUtil::throwIfNotSuccessful($response);
    }
    /**
     * Parses the response from the API endpoint to list models into a list of model metadata objects.
     *
     * @since 0.1.0
     *
     * @param Response $response The response from the API endpoint to list models.
     * @return list<ModelMetadata> List of model metadata objects.
     */
    abstract protected function parseResponseToModelMetadataList(Response $response): array;
}
                                                                                                                                                                                                                                                                     Providers/ProviderRegistry.php                                                                      0000644                 00000057020 15227644447 0012600 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers;

use WordPress\AiClientDependencies\Http\Discovery\Exception\NotFoundException as DiscoveryNotFoundException;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Providers\Contracts\ProviderInterface;
use WordPress\AiClient\Providers\Contracts\ProviderWithOperationsHandlerInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\DTO\ProviderModelsMetadata;
use WordPress\AiClient\Providers\Http\Contracts\HttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\RequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\HttpTransporterFactory;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
/**
 * Registry for managing AI providers and their models.
 *
 * This class provides a centralized way to register AI providers, discover
 * their capabilities, and find suitable models based on requirements.
 *
 * @since 0.1.0
 */
class ProviderRegistry implements WithHttpTransporterInterface
{
    use WithHttpTransporterTrait {
        setHttpTransporter as setHttpTransporterOriginal;
    }
    /**
     * @var array<string, class-string<ProviderInterface>> Mapping of provider IDs to class names.
     */
    private array $registeredIdsToClassNames = [];
    /**
     * @var array<class-string<ProviderInterface>, string> Mapping of provider class names to IDs.
     */
    private array $registeredClassNamesToIds = [];
    /**
     * @var array<class-string<ProviderInterface>, RequestAuthenticationInterface> Mapping of provider class names to
     *                                                                             authentication instances.
     */
    private array $providerAuthenticationInstances = [];
    /**
     * Registers a provider class with the registry.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The fully qualified provider class name implementing the
     * ProviderInterface
     * @throws InvalidArgumentException If the class doesn't exist or implement the required interface.
     */
    public function registerProvider(string $className): void
    {
        if (!class_exists($className)) {
            throw new InvalidArgumentException(sprintf('Provider class does not exist: %s', $className));
        }
        // Validate that class implements ProviderInterface
        if (!is_subclass_of($className, ProviderInterface::class)) {
            throw new InvalidArgumentException(sprintf('Provider class must implement %s: %s', ProviderInterface::class, $className));
        }
        $metadata = $className::metadata();
        if (!$metadata instanceof ProviderMetadata) {
            throw new InvalidArgumentException(sprintf('Provider must return ProviderMetadata from metadata() method: %s', $className));
        }
        // If there is already a HTTP transporter instance set, hook it up to the provider as needed.
        try {
            $httpTransporter = $this->getHttpTransporter();
        } catch (RuntimeException $e) {
            /*
             * If this fails, it's okay. There is no defined sequence between setting the HTTP transporter in the
             * registry and registering providers in it, so it might be that the transporter is set later. It will be
             * hooked up then.
             * But for now we can ignore this exception and attempt to set the default HTTP transporter, if possible.
             */
            try {
                $this->setHttpTransporter(HttpTransporterFactory::createTransporter());
                $httpTransporter = $this->getHttpTransporter();
            } catch (DiscoveryNotFoundException $e) {
                /*
                 * If no HTTP client implementation can be discovered yet, we can ignore this for now.
                 * It might be set later, so it's not a hard error at this point.
                 * We'll try again the next time a provider is registered, or maybe by that time an explicit
                 * HTTP transporter will have been set.
                 */
            }
        }
        if (isset($httpTransporter)) {
            $this->setHttpTransporterForProvider($className, $httpTransporter);
        }
        // Hook up the request authentication instance, using a default if not set.
        if (!isset($this->providerAuthenticationInstances[$className])) {
            $defaultProviderAuthentication = $this->createDefaultProviderRequestAuthentication($className);
            if ($defaultProviderAuthentication !== null) {
                $this->providerAuthenticationInstances[$className] = $defaultProviderAuthentication;
            }
        }
        if (isset($this->providerAuthenticationInstances[$className])) {
            $this->setRequestAuthenticationForProvider($className, $this->providerAuthenticationInstances[$className]);
        }
        $this->registeredIdsToClassNames[$metadata->getId()] = $className;
        $this->registeredClassNamesToIds[$className] = $metadata->getId();
    }
    /**
     * Gets a list of all registered provider IDs.
     *
     * @since 0.1.0
     *
     * @return list<string> List of registered provider IDs.
     */
    public function getRegisteredProviderIds(): array
    {
        return array_keys($this->registeredIdsToClassNames);
    }
    /**
     * Checks if a provider is registered.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name to check.
     * @return bool True if the provider is registered.
     */
    public function hasProvider(string $idOrClassName): bool
    {
        return $this->isRegisteredId($idOrClassName) || $this->isRegisteredClassName($idOrClassName);
    }
    /**
     * Gets the class name for a registered provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return class-string<ProviderInterface> The provider class name.
     * @throws InvalidArgumentException If the provider is not registered.
     */
    public function getProviderClassName(string $idOrClassName): string
    {
        // If it's already a class name, return it
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered ID, return its class name
        if ($this->isRegisteredId($idOrClassName)) {
            return $this->registeredIdsToClassNames[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * Gets the provider ID for a registered provider.
     *
     * @since 0.2.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return string The provider ID.
     * @throws InvalidArgumentException If the provider is not registered.
     */
    public function getProviderId(string $idOrClassName): string
    {
        // If it's already an ID, return it
        if ($this->isRegisteredId($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered class name, return its ID
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $this->registeredClassNamesToIds[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * Checks if a provider is properly configured.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return bool True if the provider is configured and ready to use.
     */
    public function isProviderConfigured(string $idOrClassName): bool
    {
        try {
            $className = $this->resolveProviderClassName($idOrClassName);
            // Use static method from ProviderInterface
            /** @var class-string<ProviderInterface> $className */
            $availability = $className::availability();
            return $availability->isConfigured();
        } catch (InvalidArgumentException $e) {
            return \false;
        }
    }
    /**
     * Finds models across all available providers that support the given requirements.
     *
     * @since 0.1.0
     *
     * @param ModelRequirements $modelRequirements The requirements to match against.
     * @return list<ProviderModelsMetadata> List of provider models metadata that match requirements.
     */
    public function findModelsMetadataForSupport(ModelRequirements $modelRequirements): array
    {
        $results = [];
        foreach ($this->registeredIdsToClassNames as $providerId => $className) {
            $providerResults = $this->findProviderModelsMetadataForSupport($providerId, $modelRequirements);
            if (!empty($providerResults)) {
                // Use static method from ProviderInterface
                /** @var class-string<ProviderInterface> $className */
                $providerMetadata = $className::metadata();
                $results[] = new ProviderModelsMetadata($providerMetadata, $providerResults);
            }
        }
        return $results;
    }
    /**
     * Finds models within a specific available provider that support the given requirements.
     *
     * @since 0.1.0
     *
     * @param string $idOrClassName The provider ID or class name.
     * @param ModelRequirements $modelRequirements The requirements to match against.
     * @return list<ModelMetadata> List of model metadata that match requirements.
     */
    public function findProviderModelsMetadataForSupport(string $idOrClassName, ModelRequirements $modelRequirements): array
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        // If the provider is not configured, there is no way to use it, so it is considered unavailable.
        if (!$this->isProviderConfigured($className)) {
            return [];
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        // Filter models that meet requirements
        $matchingModels = [];
        foreach ($modelMetadataDirectory->listModelMetadata() as $modelMetadata) {
            if ($modelRequirements->areMetBy($modelMetadata)) {
                $matchingModels[] = $modelMetadata;
            }
        }
        return $matchingModels;
    }
    /**
     * Gets a configured model instance from a provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @param string $modelId The model identifier.
     * @param ModelConfig|null $modelConfig The model configuration.
     * @return ModelInterface The configured model instance.
     * @throws InvalidArgumentException If provider or model is not found.
     */
    public function getProviderModel(string $idOrClassName, string $modelId, ?ModelConfig $modelConfig = null): ModelInterface
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        $modelInstance = $className::model($modelId, $modelConfig);
        $this->bindModelDependencies($modelInstance);
        return $modelInstance;
    }
    /**
     * Binds dependencies to a model instance.
     *
     * This method injects required dependencies such as HTTP transporter
     * and authentication into model instances that need them.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $modelInstance The model instance to bind dependencies to.
     * @return void
     */
    public function bindModelDependencies(ModelInterface $modelInstance): void
    {
        $className = $this->resolveProviderClassName($modelInstance->providerMetadata()->getId());
        if ($modelInstance instanceof WithHttpTransporterInterface) {
            $modelInstance->setHttpTransporter($this->getHttpTransporter());
        }
        if ($modelInstance instanceof WithRequestAuthenticationInterface) {
            $requestAuthentication = $this->getProviderRequestAuthentication($className);
            if ($requestAuthentication !== null) {
                $modelInstance->setRequestAuthentication($requestAuthentication);
            }
        }
    }
    /**
     * Gets the class name for a registered provider (handles both ID and class name input).
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return class-string<ProviderInterface> The provider class name.
     * @throws InvalidArgumentException If provider is not registered.
     */
    private function resolveProviderClassName(string $idOrClassName): string
    {
        // If it's already a class name, return it
        if ($this->isRegisteredClassName($idOrClassName)) {
            return $idOrClassName;
        }
        // If it's a registered ID, return its class name
        if ($this->isRegisteredId($idOrClassName)) {
            return $this->registeredIdsToClassNames[$idOrClassName];
        }
        // Not found
        throw new InvalidArgumentException(sprintf('Provider not registered: %s', $idOrClassName));
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function setHttpTransporter(HttpTransporterInterface $httpTransporter): void
    {
        $this->setHttpTransporterOriginal($httpTransporter);
        // Make sure all registered providers have the HTTP transporter hooked up as needed.
        foreach ($this->registeredIdsToClassNames as $className) {
            $this->setHttpTransporterForProvider($className, $httpTransporter);
        }
    }
    /**
     * Sets the request authentication instance for the given provider.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @param RequestAuthenticationInterface $requestAuthentication The request authentication instance.
     */
    public function setProviderRequestAuthentication(string $idOrClassName, RequestAuthenticationInterface $requestAuthentication): void
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        $this->providerAuthenticationInstances[$className] = $requestAuthentication;
        $this->setRequestAuthenticationForProvider($className, $requestAuthentication);
    }
    /**
     * Gets the request authentication instance for the given provider, if set.
     *
     * @since 0.1.0
     *
     * @param string|class-string<ProviderInterface> $idOrClassName The provider ID or class name.
     * @return ?RequestAuthenticationInterface The request authentication instance, or null if not set.
     */
    public function getProviderRequestAuthentication(string $idOrClassName): ?RequestAuthenticationInterface
    {
        $className = $this->resolveProviderClassName($idOrClassName);
        if (!isset($this->providerAuthenticationInstances[$className])) {
            return null;
        }
        return $this->providerAuthenticationInstances[$className];
    }
    /**
     * Sets the HTTP transporter for a specific provider, hooking up its class instances.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @param HttpTransporterInterface $httpTransporter The HTTP transporter instance.
     */
    private function setHttpTransporterForProvider(string $className, HttpTransporterInterface $httpTransporter): void
    {
        $availability = $className::availability();
        if ($availability instanceof WithHttpTransporterInterface) {
            $availability->setHttpTransporter($httpTransporter);
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        if ($modelMetadataDirectory instanceof WithHttpTransporterInterface) {
            $modelMetadataDirectory->setHttpTransporter($httpTransporter);
        }
        if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
            $operationsHandler = $className::operationsHandler();
            if ($operationsHandler instanceof WithHttpTransporterInterface) {
                $operationsHandler->setHttpTransporter($httpTransporter);
            }
        }
    }
    /**
     * Sets the request authentication for a specific provider, hooking up its class instances.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @param RequestAuthenticationInterface $requestAuthentication The authentication instance.
     *
     * @throws InvalidArgumentException If the authentication instance is not of the expected type.
     */
    private function setRequestAuthenticationForProvider(string $className, RequestAuthenticationInterface $requestAuthentication): void
    {
        $authenticationMethod = $className::metadata()->getAuthenticationMethod();
        if ($authenticationMethod === null) {
            throw new InvalidArgumentException(sprintf('Provider %s does not expect any authentication, but got %s.', $className, get_class($requestAuthentication)));
        }
        $expectedClass = $authenticationMethod->getImplementationClass();
        if (!$requestAuthentication instanceof $expectedClass) {
            throw new InvalidArgumentException(sprintf('Provider %s expects authentication of type %s, but got %s.', $className, $expectedClass, get_class($requestAuthentication)));
        }
        $availability = $className::availability();
        if ($availability instanceof WithRequestAuthenticationInterface) {
            $availability->setRequestAuthentication($requestAuthentication);
        }
        $modelMetadataDirectory = $className::modelMetadataDirectory();
        if ($modelMetadataDirectory instanceof WithRequestAuthenticationInterface) {
            $modelMetadataDirectory->setRequestAuthentication($requestAuthentication);
        }
        if (is_subclass_of($className, ProviderWithOperationsHandlerInterface::class)) {
            $operationsHandler = $className::operationsHandler();
            if ($operationsHandler instanceof WithRequestAuthenticationInterface) {
                $operationsHandler->setRequestAuthentication($requestAuthentication);
            }
        }
    }
    /**
     * Creates a default request authentication instance for a provider.
     *
     * @since 0.1.0
     *
     * @param class-string<ProviderInterface> $className The provider class name.
     * @return ?RequestAuthenticationInterface The default request authentication instance, or null if not required or
     *                                         if no credential data can be found.
     */
    private function createDefaultProviderRequestAuthentication(string $className): ?RequestAuthenticationInterface
    {
        $providerMetadata = $className::metadata();
        $providerId = $providerMetadata->getId();
        $authenticationMethod = $providerMetadata->getAuthenticationMethod();
        if ($authenticationMethod === null) {
            return null;
        }
        $authenticationClass = $authenticationMethod->getImplementationClass();
        if ($authenticationClass === null) {
            return null;
        }
        $authenticationSchema = $authenticationClass::getJsonSchema();
        // Iterate over all JSON schema object properties to try to determine the necessary authentication data.
        $authenticationData = [];
        if (isset($authenticationSchema['properties']) && is_array($authenticationSchema['properties'])) {
            /** @var array<string, mixed> $details */
            foreach ($authenticationSchema['properties'] as $property => $details) {
                $envVarName = $this->getEnvVarName($providerId, $property);
                // Try to get the value from environment variable or constant.
                $envValue = getenv($envVarName);
                if ($envValue === \false) {
                    if (!defined($envVarName)) {
                        continue;
                        // Skip if neither environment variable nor constant is defined.
                    }
                    $envValue = constant($envVarName);
                    if (!is_scalar($envValue)) {
                        continue;
                    }
                }
                if (isset($details['type'])) {
                    switch ($details['type']) {
                        case 'boolean':
                            $authenticationData[$property] = filter_var($envValue, \FILTER_VALIDATE_BOOLEAN);
                            break;
                        case 'number':
                            $authenticationData[$property] = (int) $envValue;
                            break;
                        case 'string':
                        default:
                            $authenticationData[$property] = (string) $envValue;
                    }
                } else {
                    // Default to string if no type is specified.
                    $authenticationData[$property] = (string) $envValue;
                }
            }
            // If any required fields are missing, return null to avoid immediate errors.
            if (isset($authenticationSchema['required']) && is_array($authenticationSchema['required'])) {
                /** @var list<string> $requiredProperties */
                $requiredProperties = $authenticationSchema['required'];
                if (array_diff_key(array_flip($requiredProperties), $authenticationData)) {
                    return null;
                }
            }
        }
        /** @var RequestAuthenticationInterface */
        /** @var array<string, mixed> $authenticationData */
        return $authenticationClass::fromArray($authenticationData);
    }
    /**
     * Checks if the given value is a registered provider class name.
     *
     * @since 0.4.0
     *
     * @param string $idOrClassName The value to check.
     * @return bool True if it's a registered class name.
     * @phpstan-assert-if-true class-string<ProviderInterface> $idOrClassName
     */
    private function isRegisteredClassName(string $idOrClassName): bool
    {
        return isset($this->registeredClassNamesToIds[$idOrClassName]);
    }
    /**
     * Checks if the given value is a registered provider ID.
     *
     * @since 0.4.0
     *
     * @param string $idOrClassName The value to check.
     * @return bool True if it's a registered provider ID.
     */
    private function isRegisteredId(string $idOrClassName): bool
    {
        return isset($this->registeredIdsToClassNames[$idOrClassName]);
    }
    /**
     * Converts a provider ID and field name to a constant case environment variable name.
     *
     * @since 0.1.0
     *
     * @param string $providerId The provider ID.
     * @param string $field The field name.
     * @return string The environment variable name in CONSTANT_CASE.
     */
    private function getEnvVarName(string $providerId, string $field): string
    {
        // Convert camelCase or kebab-case or snake_case to CONSTANT_CASE.
        $constantCaseProviderId = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $providerId)));
        $constantCaseField = strtoupper((string) preg_replace('/([a-z])([A-Z])/', '$1_$2', str_replace('-', '_', $field)));
        return "{$constantCaseProviderId}_{$constantCaseField}";
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                Providers/ApiBasedImplementation/AbstractApiProvider.php                                            0000644                 00000003001 15227644447 0017531 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\Providers\AbstractProvider;
/**
 * Base class for API-based providers.
 *
 * This abstract class provides URL construction utilities for providers that
 * communicate with REST APIs. It standardizes the pattern of combining a base
 * URL with endpoint paths.
 *
 * @since 0.2.0
 */
abstract class AbstractApiProvider extends AbstractProvider
{
    /**
     * Gets the base URL for the provider's API.
     *
     * The base URL should include the protocol and domain, and may include
     * the API version path (e.g., "https://api.example.com/v1").
     *
     * @since 0.2.0
     *
     * @return string The base URL for the provider's API.
     */
    abstract protected static function baseUrl(): string;
    /**
     * Constructs a full URL by combining the base URL with an optional path.
     *
     * This method ensures proper URL construction by:
     * - Using the provider's base URL
     * - Trimming leading slashes from the path to prevent double-slashes
     * - Joining the base URL and path with a single forward slash
     *
     * @since 0.2.0
     *
     * @param string $path Optional path to append to the base URL. Default empty string.
     * @return string The complete URL.
     */
    public static function url(string $path = ''): string
    {
        if ($path === '') {
            return static::baseUrl();
        }
        return static::baseUrl() . '/' . ltrim($path, '/');
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php                         0000644                 00000003406 15227644447 0023350 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use Exception;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
/**
 * Class to check availability for an API-based provider via a test request to the endpoint to list models.
 *
 * This class should be used for cloud-based providers that offer a model listing endpoint which requires
 * authentication. A request to this endpoint is used to determine if the provider is properly configured
 * with valid credentials.
 *
 * @since 0.1.0
 */
class ListModelsApiBasedProviderAvailability implements ProviderAvailabilityInterface
{
    /**
     * @var ModelMetadataDirectoryInterface The model metadata directory to use for checking availability.
     */
    private ModelMetadataDirectoryInterface $modelMetadataDirectory;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelMetadataDirectoryInterface $modelMetadataDirectory The model metadata directory to use for checking
     *                                                                availability.
     */
    public function __construct(ModelMetadataDirectoryInterface $modelMetadataDirectory)
    {
        $this->modelMetadataDirectory = $modelMetadataDirectory;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function isConfigured(): bool
    {
        try {
            // Attempt to list models to check if the provider is available.
            $this->modelMetadataDirectory->listModelMetadata();
            return \true;
        } catch (Exception $e) {
            // If an exception occurs, the provider is not available.
            return \false;
        }
    }
}
                                                                                                                                                                                                                                                          Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php                       0000644                 00000004500 15227644447 0023664 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use Exception;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
/**
 * Class to check availability for an API-based provider via a test request to the endpoint to generate text.
 *
 * This class should be used for cloud-based providers that do not offer a model listing endpoint, but do offer a
 * text generation endpoint which requires authentication. A minimal request to this endpoint is used to determine
 * if the provider is properly configured with valid credentials.
 *
 * @since 0.1.0
 */
class GenerateTextApiBasedProviderAvailability implements ProviderAvailabilityInterface
{
    /**
     * @var ModelInterface&TextGenerationModelInterface The model to use for checking availability.
     */
    private ModelInterface $model;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to use for checking availability.
     */
    public function __construct(ModelInterface $model)
    {
        if (!$model instanceof TextGenerationModelInterface) {
            throw new Exception('The model class to check provider availability must implement TextGenerationModelInterface.');
        }
        $this->model = $model;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    public function isConfigured(): bool
    {
        // Set config to use as few resources as possible for the test.
        $modelConfig = ModelConfig::fromArray([ModelConfig::KEY_MAX_TOKENS => 1]);
        $this->model->setConfig($modelConfig);
        try {
            // Attempt to generate text to check if the provider is available.
            $this->model->generateTextResult([new Message(MessageRoleEnum::user(), [new MessagePart('a')])]);
            return \true;
        } catch (Exception $e) {
            // If an exception occurs, the provider is not available.
            return \false;
        }
    }
}
                                                                                                                                                                                                Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php                               0000644                 00000002021 15227644447 0022034 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation\Contracts;

use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
/**
 * Interface for API-based AI models that support HTTP transport configuration.
 *
 * This interface extends ModelInterface to add request options support
 * for models that communicate with external APIs via HTTP.
 *
 * @since 0.3.0
 */
interface ApiBasedModelInterface extends ModelInterface
{
    /**
     * Sets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @param RequestOptions $requestOptions The request options to use.
     * @return void
     */
    public function setRequestOptions(RequestOptions $requestOptions): void;
    /**
     * Gets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @return RequestOptions|null The request options, or null if not set.
     */
    public function getRequestOptions(): ?RequestOptions;
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               Providers/ApiBasedImplementation/Contracts/error_log                                                0000644                 00000001620 15227644447 0016772 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [18-Jul-2026 17:19:18 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Models\Contracts\ModelInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php on line 16
[19-Jul-2026 23:50:54 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Models\Contracts\ModelInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/Contracts/ApiBasedModelInterface.php on line 16
                                                                                                                Providers/ApiBasedImplementation/error_log                                                          0000644                 00000010310 15227644447 0015026 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       [18-Jul-2026 17:18:58 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php on line 18
[19-Jul-2026 05:17:57 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php on line 21
[19-Jul-2026 05:17:57 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiBasedModel.php on line 23
[19-Jul-2026 08:18:03 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\AbstractProvider" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiProvider.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiProvider.php on line 16
[19-Jul-2026 08:18:04 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php on line 23
[19-Jul-2026 23:50:53 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php:18
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/ListModelsApiBasedProviderAvailability.php on line 18
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php on line 21
[20-Jul-2026 08:44:34 UTC] PHP Fatal error:  Trait "WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiBasedModel.php on line 23
[20-Jul-2026 11:44:08 UTC] PHP Fatal error:  Uncaught Error: Class "WordPress\AiClient\Providers\AbstractProvider" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiProvider.php:16
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/AbstractApiProvider.php on line 16
[20-Jul-2026 11:44:09 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php on line 23
[20-Jul-2026 13:18:42 UTC] PHP Fatal error:  Uncaught Error: Interface "WordPress\AiClient\Providers\Contracts\ProviderAvailabilityInterface" not found in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php:23
Stack trace:
#0 {main}
  thrown in /home/centerfor3d/public_html/wp-includes/php-ai-client/src/Providers/ApiBasedImplementation/GenerateTextApiBasedProviderAvailability.php on line 23
                                                                                                                                                                                                                                                                                                                        Providers/ApiBasedImplementation/AbstractApiBasedModelMetadataDirectory.php                         0000644                 00000006365 15227644447 0023304 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\AiClient;
use WordPress\AiClient\Common\Contracts\CachesDataInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Traits\WithDataCachingTrait;
use WordPress\AiClient\Providers\Contracts\ModelMetadataDirectoryInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Http\Traits\WithRequestAuthenticationTrait;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for an API-based model metadata directory for a provider.
 *
 * @since 0.1.0
 */
abstract class AbstractApiBasedModelMetadataDirectory implements ModelMetadataDirectoryInterface, WithHttpTransporterInterface, WithRequestAuthenticationInterface, CachesDataInterface
{
    use WithHttpTransporterTrait;
    use WithRequestAuthenticationTrait;
    use WithDataCachingTrait;
    /**
     * The cache key suffix for the models list.
     *
     * @since 0.4.0
     *
     * @var string
     */
    private const MODELS_CACHE_KEY = 'models';
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function listModelMetadata(): array
    {
        $modelsMetadata = $this->getModelMetadataMap();
        return array_values($modelsMetadata);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function hasModelMetadata(string $modelId): bool
    {
        $modelsMetadata = $this->getModelMetadataMap();
        return isset($modelsMetadata[$modelId]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function getModelMetadata(string $modelId): ModelMetadata
    {
        $modelsMetadata = $this->getModelMetadataMap();
        if (!isset($modelsMetadata[$modelId])) {
            throw new InvalidArgumentException(sprintf('No model with ID %s was found in the provider', $modelId));
        }
        return $modelsMetadata[$modelId];
    }
    /**
     * Returns the map of model ID to model metadata for all models from the provider.
     *
     * @since 0.1.0
     *
     * @return array<string, ModelMetadata> Map of model ID to model metadata.
     */
    private function getModelMetadataMap(): array
    {
        /** @var array<string, ModelMetadata> */
        return $this->cached(self::MODELS_CACHE_KEY, fn() => $this->sendListModelsRequest(), 86400);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.4.0
     */
    protected function getCachedKeys(): array
    {
        return [self::MODELS_CACHE_KEY];
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.4.0
     */
    protected function getBaseCacheKey(): string
    {
        return 'ai_client_' . AiClient::VERSION . '_' . md5(static::class);
    }
    /**
     * Sends the API request to list models from the provider and returns the map of model ID to model metadata.
     *
     * @since 0.1.0
     *
     * @return array<string, ModelMetadata> Map of model ID to model metadata.
     */
    abstract protected function sendListModelsRequest(): array;
}
                                                                                                                                                                                                                                                                           Providers/ApiBasedImplementation/AbstractApiBasedModel.php                                          0000644                 00000006231 15227644447 0017746 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Providers\ApiBasedImplementation;

use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
use WordPress\AiClient\Providers\DTO\ProviderMetadata;
use WordPress\AiClient\Providers\Http\Contracts\WithHttpTransporterInterface;
use WordPress\AiClient\Providers\Http\Contracts\WithRequestAuthenticationInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Http\Traits\WithHttpTransporterTrait;
use WordPress\AiClient\Providers\Http\Traits\WithRequestAuthenticationTrait;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
/**
 * Base class for an API-based model for a provider.
 *
 * While this class contains no abstract methods, it is still abstract to ensure that each model class can actually
 * perform generative AI tasks by implementing the corresponding interfaces.
 *
 * @since 0.1.0
 */
abstract class AbstractApiBasedModel implements ApiBasedModelInterface, WithHttpTransporterInterface, WithRequestAuthenticationInterface
{
    use WithHttpTransporterTrait;
    use WithRequestAuthenticationTrait;
    /**
     * @var ModelMetadata The metadata for the model.
     */
    private ModelMetadata $metadata;
    /**
     * @var ProviderMetadata The metadata for the model's provider.
     */
    private ProviderMetadata $providerMetadata;
    /**
     * @var ModelConfig The configuration for the model.
     */
    private ModelConfig $config;
    /**
     * @var RequestOptions|null The request options for HTTP transport.
     */
    private ?RequestOptions $requestOptions = null;
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ModelMetadata $metadata The metadata for the model.
     * @param ProviderMetadata $providerMetadata The metadata for the model's provider.
     */
    public function __construct(ModelMetadata $metadata, ProviderMetadata $providerMetadata)
    {
        $this->metadata = $metadata;
        $this->providerMetadata = $providerMetadata;
        $this->config = ModelConfig::fromArray([]);
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function metadata(): ModelMetadata
    {
        return $this->metadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function providerMetadata(): ProviderMetadata
    {
        return $this->providerMetadata;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function setConfig(ModelConfig $config): void
    {
        $this->config = $config;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.1.0
     */
    final public function getConfig(): ModelConfig
    {
        return $this->config;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.3.0
     */
    final public function setRequestOptions(RequestOptions $requestOptions): void
    {
        $this->requestOptions = $requestOptions;
    }
    /**
     * {@inheritDoc}
     *
     * @since 0.3.0
     */
    final public function getRequestOptions(): ?RequestOptions
    {
        return $this->requestOptions;
    }
}
                                                                                                                                                                                                                                                                                                                                                                       Builders/MessageBuilder.php                                                                         0000644                 00000014672 15227644447 0011752 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Builders;

use InvalidArgumentException;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Tools\DTO\FunctionCall;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
/**
 * Fluent builder for constructing AI messages.
 *
 * This class provides a fluent interface for building messages with various
 * content types including text, files, function calls, and function responses.
 *
 * @since 0.2.0
 *
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type Input string|MessagePart|MessagePartArrayShape|File|FunctionCall|FunctionResponse|null
 */
class MessageBuilder
{
    /**
     * @var MessageRoleEnum|null The role of the message sender.
     */
    protected ?MessageRoleEnum $role = null;
    /**
     * @var list<MessagePart> The parts that make up the message.
     */
    protected array $parts = [];
    /**
     * Constructor.
     *
     * @since 0.2.0
     *
     * @param Input $input Optional initial content.
     * @param MessageRoleEnum|null $role Optional role.
     */
    public function __construct($input = null, ?MessageRoleEnum $role = null)
    {
        $this->role = $role;
        if ($input === null) {
            return;
        }
        // Handle different input types
        if ($input instanceof MessagePart) {
            $this->parts[] = $input;
        } elseif (is_string($input)) {
            $this->withText($input);
        } elseif ($input instanceof File) {
            $this->withFile($input);
        } elseif ($input instanceof FunctionCall) {
            $this->withFunctionCall($input);
        } elseif ($input instanceof FunctionResponse) {
            $this->withFunctionResponse($input);
        } elseif (is_array($input) && MessagePart::isArrayShape($input)) {
            $this->parts[] = MessagePart::fromArray($input);
        } else {
            throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, File, FunctionCall, or FunctionResponse.');
        }
    }
    /**
     * Creates a deep clone of this builder.
     *
     * Clones all MessagePart objects in the parts array to ensure
     * the cloned builder is independent of the original.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone parts array (MessagePart has __clone)
        $clonedParts = [];
        foreach ($this->parts as $part) {
            $clonedParts[] = clone $part;
        }
        $this->parts = $clonedParts;
        // Note: $role is an enum value object and can be safely shared
    }
    /**
     * Sets the role of the message sender.
     *
     * @since 0.2.0
     *
     * @param MessageRoleEnum $role The role to set.
     * @return self
     */
    public function usingRole(MessageRoleEnum $role): self
    {
        $this->role = $role;
        return $this;
    }
    /**
     * Sets the role to user.
     *
     * @since 0.2.0
     *
     * @return self
     */
    public function usingUserRole(): self
    {
        return $this->usingRole(MessageRoleEnum::user());
    }
    /**
     * Sets the role to model.
     *
     * @since 0.2.0
     *
     * @return self
     */
    public function usingModelRole(): self
    {
        return $this->usingRole(MessageRoleEnum::model());
    }
    /**
     * Adds text content to the message.
     *
     * @since 0.2.0
     *
     * @param string $text The text to add.
     * @return self
     * @throws InvalidArgumentException If the text is empty.
     */
    public function withText(string $text): self
    {
        if (trim($text) === '') {
            throw new InvalidArgumentException('Text content cannot be empty.');
        }
        $this->parts[] = new MessagePart($text);
        return $this;
    }
    /**
     * Adds a file to the message.
     *
     * Accepts:
     * - File object
     * - URL string (remote file)
     * - Base64-encoded data string
     * - Data URI string (data:mime/type;base64,data)
     * - Local file path string
     *
     * @since 0.2.0
     *
     * @param string|File $file The file to add.
     * @param string|null $mimeType Optional MIME type (ignored if File object provided).
     * @return self
     * @throws InvalidArgumentException If the file is invalid.
     */
    public function withFile($file, ?string $mimeType = null): self
    {
        $file = $file instanceof File ? $file : new File($file, $mimeType);
        $this->parts[] = new MessagePart($file);
        return $this;
    }
    /**
     * Adds a function call to the message.
     *
     * @since 0.2.0
     *
     * @param FunctionCall $functionCall The function call to add.
     * @return self
     */
    public function withFunctionCall(FunctionCall $functionCall): self
    {
        $this->parts[] = new MessagePart($functionCall);
        return $this;
    }
    /**
     * Adds a function response to the message.
     *
     * @since 0.2.0
     *
     * @param FunctionResponse $functionResponse The function response to add.
     * @return self
     */
    public function withFunctionResponse(FunctionResponse $functionResponse): self
    {
        $this->parts[] = new MessagePart($functionResponse);
        return $this;
    }
    /**
     * Adds multiple message parts to the message.
     *
     * @since 0.2.0
     *
     * @param MessagePart ...$parts The message parts to add.
     * @return self
     */
    public function withMessageParts(MessagePart ...$parts): self
    {
        foreach ($parts as $part) {
            $this->parts[] = $part;
        }
        return $this;
    }
    /**
     * Builds and returns the Message object.
     *
     * @since 0.2.0
     *
     * @return Message The built message.
     * @throws InvalidArgumentException If the message validation fails.
     */
    public function get(): Message
    {
        if (empty($this->parts)) {
            throw new InvalidArgumentException('Cannot build an empty message. Add content using withText() or similar methods.');
        }
        if ($this->role === null) {
            throw new InvalidArgumentException('Cannot build a message with no role. Set a role using usingRole() or similar methods.');
        }
        // At this point, we've validated that $this->role is not null
        /** @var MessageRoleEnum $role */
        $role = $this->role;
        return new Message($role, $this->parts);
    }
}
                                                                      Builders/PromptBuilder.php                                                                          0000644                 00000153503 15227644447 0011644 0                                                                                                    ustar 00                                                                                                                                                                                                                                                       <?php

declare (strict_types=1);
namespace WordPress\AiClient\Builders;

use WordPress\AiClientDependencies\Psr\EventDispatcher\EventDispatcherInterface;
use WordPress\AiClient\Common\Exception\InvalidArgumentException;
use WordPress\AiClient\Common\Exception\RuntimeException;
use WordPress\AiClient\Events\AfterGenerateResultEvent;
use WordPress\AiClient\Events\BeforeGenerateResultEvent;
use WordPress\AiClient\Files\DTO\File;
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
use WordPress\AiClient\Messages\DTO\Message;
use WordPress\AiClient\Messages\DTO\MessagePart;
use WordPress\AiClient\Messages\DTO\UserMessage;
use WordPress\AiClient\Messages\Enums\MessageRoleEnum;
use WordPress\AiClient\Messages\Enums\ModalityEnum;
use WordPress\AiClient\Providers\ApiBasedImplementation\Contracts\ApiBasedModelInterface;
use WordPress\AiClient\Providers\Http\DTO\RequestOptions;
use WordPress\AiClient\Providers\Models\Contracts\ModelInterface;
use WordPress\AiClient\Providers\Models\DTO\ModelConfig;
use WordPress\AiClient\Providers\Models\DTO\ModelMetadata;
use WordPress\AiClient\Providers\Models\DTO\ModelRequirements;
use WordPress\AiClient\Providers\Models\Enums\CapabilityEnum;
use WordPress\AiClient\Providers\Models\ImageGeneration\Contracts\ImageGenerationModelInterface;
use WordPress\AiClient\Providers\Models\SpeechGeneration\Contracts\SpeechGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextGeneration\Contracts\TextGenerationModelInterface;
use WordPress\AiClient\Providers\Models\TextToSpeechConversion\Contracts\TextToSpeechConversionModelInterface;
use WordPress\AiClient\Providers\Models\VideoGeneration\Contracts\VideoGenerationModelInterface;
use WordPress\AiClient\Providers\ProviderRegistry;
use WordPress\AiClient\Results\DTO\GenerativeAiResult;
use WordPress\AiClient\Tools\DTO\FunctionDeclaration;
use WordPress\AiClient\Tools\DTO\FunctionResponse;
use WordPress\AiClient\Tools\DTO\WebSearch;
/**
 * Fluent builder for constructing AI prompts.
 *
 * This class provides a fluent interface for building prompts with various
 * content types and model configurations. It automatically infers model
 * requirements based on the features used in the prompt.
 *
 * @since 0.1.0
 *
 * @phpstan-import-type MessageArrayShape from Message
 * @phpstan-import-type MessagePartArrayShape from MessagePart
 *
 * @phpstan-type Prompt string|MessagePart|Message|MessageArrayShape|list<string|MessagePart|MessagePartArrayShape>|list<Message>|null
 */
class PromptBuilder
{
    /**
     * @var ProviderRegistry The provider registry for finding suitable models.
     */
    private ProviderRegistry $registry;
    /**
     * @var list<Message> The messages in the conversation.
     */
    protected array $messages = [];
    /**
     * @var ModelInterface|null The model to use for generation.
     */
    protected ?ModelInterface $model = null;
    /**
     * @var list<string> Ordered list of preference keys to check when selecting a model.
     */
    protected array $modelPreferenceKeys = [];
    /**
     * @var string|null The provider ID or class name.
     */
    protected ?string $providerIdOrClassName = null;
    /**
     * @var ModelConfig The model configuration.
     */
    protected ModelConfig $modelConfig;
    /**
     * @var RequestOptions|null The request options for HTTP transport.
     */
    protected ?RequestOptions $requestOptions = null;
    /**
     * @var EventDispatcherInterface|null The event dispatcher for prompt lifecycle events.
     */
    private ?EventDispatcherInterface $eventDispatcher = null;
    // phpcs:disable Generic.Files.LineLength.TooLong
    /**
     * Constructor.
     *
     * @since 0.1.0
     *
     * @param ProviderRegistry $registry The provider registry for finding suitable models.
     * @param Prompt $prompt Optional initial prompt content.
     * @param EventDispatcherInterface|null $eventDispatcher Optional event dispatcher for lifecycle events.
     */
    // phpcs:enable Generic.Files.LineLength.TooLong
    public function __construct(ProviderRegistry $registry, $prompt = null, ?EventDispatcherInterface $eventDispatcher = null)
    {
        $this->registry = $registry;
        $this->modelConfig = new ModelConfig();
        $this->eventDispatcher = $eventDispatcher;
        if ($prompt === null) {
            return;
        }
        // Check if it's a list of Messages - set as messages
        if ($this->isMessagesList($prompt)) {
            $this->messages = $prompt;
            return;
        }
        // Parse it as a user message
        $userMessage = $this->parseMessage($prompt, MessageRoleEnum::user());
        $this->messages[] = $userMessage;
    }
    /**
     * Creates a deep clone of this builder.
     *
     * Clones all mutable state including messages, model configuration, and request options.
     * Service objects (registry, model, event dispatcher) are intentionally NOT cloned
     * as they are shared dependencies.
     *
     * @since 0.4.2
     */
    public function __clone()
    {
        // Deep clone messages array (Message has __clone)
        $clonedMessages = [];
        foreach ($this->messages as $message) {
            $clonedMessages[] = clone $message;
        }
        $this->messages = $clonedMessages;
        // Clone model config (ModelConfig has __clone)
        $this->modelConfig = clone $this->modelConfig;
        // Clone request options if set (contains only primitives)
        if ($this->requestOptions !== null) {
            $this->requestOptions = clone $this->requestOptions;
        }
        // Note: $registry, $model, and $eventDispatcher are service objects
        // and are intentionally NOT cloned - they should be shared references.
    }
    /**
     * Adds text to the current message.
     *
     * @since 0.1.0
     *
     * @param string $text The text to add.
     * @return self
     */
    public function withText(string $text): self
    {
        $part = new MessagePart($text);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds a file to the current message.
     *
     * Accepts:
     * - File object
     * - URL string (remote file)
     * - Base64-encoded data string
     * - Data URI string (data:mime/type;base64,data)
     * - Local file path string
     *
     * @since 0.1.0
     *
     * @param string|File $file The file (File object or string representation).
     * @param string|null $mimeType The MIME type (optional, ignored if File object provided).
     * @return self
     * @throws InvalidArgumentException If the file is invalid or MIME type cannot be determined.
     */
    public function withFile($file, ?string $mimeType = null): self
    {
        $file = $file instanceof File ? $file : new File($file, $mimeType);
        $part = new MessagePart($file);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds a function response to the current message.
     *
     * @since 0.1.0
     *
     * @param FunctionResponse $functionResponse The function response.
     * @return self
     */
    public function withFunctionResponse(FunctionResponse $functionResponse): self
    {
        $part = new MessagePart($functionResponse);
        $this->appendPartToMessages($part);
        return $this;
    }
    /**
     * Adds message parts to the current message.
     *
     * @since 0.1.0
     *
     * @param MessagePart ...$parts The message parts to add.
     * @return self
     */
    public function withMessageParts(MessagePart ...$parts): self
    {
        foreach ($parts as $part) {
            $this->appendPartToMessages($part);
        }
        return $this;
    }
    /**
     * Adds conversation history messages.
     *
     * Historical messages are prepended to the beginning of the message list,
     * before the current message being built.
     *
     * @since 0.1.0
     *
     * @param Message ...$messages The messages to add to history.
     * @return self
     */
    public function withHistory(Message ...$messages): self
    {
        // Prepend the history messages to the beginning of the messages array
        $this->messages = array_merge($messages, $this->messages);
        return $this;
    }
    /**
     * Sets the model to use for generation.
     *
     * The model's configuration will be merged with the builder's configuration,
     * with the builder's configuration taking precedence for any overlapping settings.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to use.
     * @return self
     */
    public function usingModel(ModelInterface $model): self
    {
        $this->model = $model;
        // Merge model's config with builder's config, with builder's config taking precedence
        $modelConfigArray = $model->getConfig()->toArray();
        $builderConfigArray = $this->modelConfig->toArray();
        $mergedConfigArray = array_merge($modelConfigArray, $builderConfigArray);
        $this->modelConfig = ModelConfig::fromArray($mergedConfigArray);
        return $this;
    }
    /**
     * Sets preferred models to evaluate in order.
     *
     * @since 0.2.0
     *
     * @param string|ModelInterface|array{0:string,1:string} ...$preferredModels The preferred models as model IDs,
     * model instances, or [provider ID, model ID] tuples. For broader compatibility, it is recommended you specify
     * only model IDs or model instances, as that will allow for different providers that expose the same model to be
     * considered.
     * @return self
     *
     * @throws InvalidArgumentException When a preferred model has an invalid type or identifier.
     */
    public function usingModelPreference(...$preferredModels): self
    {
        if ($preferredModels === []) {
            throw new InvalidArgumentException('At least one model preference must be provided.');
        }
        $preferenceKeys = [];
        foreach ($preferredModels as $preferredModel) {
            if (is_array($preferredModel)) {
                // [model identifier, provider ID] tuple
                if (!array_is_list($preferredModel) || count($preferredModel) !== 2) {
                    throw new InvalidArgumentException('Model preference tuple must contain model identifier and provider ID.');
                }
                [$providerId, $modelId] = $preferredModel;
                $modelId = $this->normalizePreferenceIdentifier($modelId);
                $providerId = $this->normalizePreferenceIdentifier($providerId, 'Model preference provider identifiers cannot be empty.');
                $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            } elseif ($preferredModel instanceof ModelInterface) {
                // Model instance
                $modelId = $preferredModel->metadata()->getId();
                $providerId = $preferredModel->providerMetadata()->getId();
                $preferenceKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            } elseif (is_string($preferredModel)) {
                // Model ID
                $modelId = $this->normalizePreferenceIdentifier($preferredModel);
                $preferenceKey = $this->createModelPreferenceKey($modelId);
            } else {
                // Invalid type
                throw new InvalidArgumentException('Model preferences must be model identifiers, instances of ModelInterface, ' . 'or provider/model tuples.');
            }
            $preferenceKeys[] = $preferenceKey;
        }
        $this->modelPreferenceKeys = $preferenceKeys;
        return $this;
    }
    /**
     * Sets the model configuration.
     *
     * Merges the provided configuration with the builder's configuration,
     * with builder configuration taking precedence.
     *
     * @since 0.1.0
     *
     * @param ModelConfig $config The model configuration to merge.
     * @return self
     */
    public function usingModelConfig(ModelConfig $config): self
    {
        // Convert both configs to arrays
        $builderConfigArray = $this->modelConfig->toArray();
        $providedConfigArray = $config->toArray();
        // Merge arrays with builder config taking precedence
        $mergedArray = array_merge($providedConfigArray, $builderConfigArray);
        // Create new config from merged array
        $this->modelConfig = ModelConfig::fromArray($mergedArray);
        return $this;
    }
    /**
     * Sets the provider to use for generation.
     *
     * @since 0.1.0
     *
     * @param string $providerIdOrClassName The provider ID or class name.
     * @return self
     */
    public function usingProvider(string $providerIdOrClassName): self
    {
        $this->providerIdOrClassName = $providerIdOrClassName;
        return $this;
    }
    /**
     * Sets the system instruction.
     *
     * System instructions are stored in the model configuration and guide
     * the AI model's behavior throughout the conversation.
     *
     * @since 0.1.0
     *
     * @param string $systemInstruction The system instruction text.
     * @return self
     */
    public function usingSystemInstruction(string $systemInstruction): self
    {
        $this->modelConfig->setSystemInstruction($systemInstruction);
        return $this;
    }
    /**
     * Sets the maximum number of tokens to generate.
     *
     * @since 0.1.0
     *
     * @param int $maxTokens The maximum number of tokens.
     * @return self
     */
    public function usingMaxTokens(int $maxTokens): self
    {
        $this->modelConfig->setMaxTokens($maxTokens);
        return $this;
    }
    /**
     * Sets the temperature for generation.
     *
     * @since 0.1.0
     *
     * @param float $temperature The temperature value.
     * @return self
     */
    public function usingTemperature(float $temperature): self
    {
        $this->modelConfig->setTemperature($temperature);
        return $this;
    }
    /**
     * Sets the top-p value for generation.
     *
     * @since 0.1.0
     *
     * @param float $topP The top-p value.
     * @return self
     */
    public function usingTopP(float $topP): self
    {
        $this->modelConfig->setTopP($topP);
        return $this;
    }
    /**
     * Sets the top-k value for generation.
     *
     * @since 0.1.0
     *
     * @param int $topK The top-k value.
     * @return self
     */
    public function usingTopK(int $topK): self
    {
        $this->modelConfig->setTopK($topK);
        return $this;
    }
    /**
     * Sets stop sequences for generation.
     *
     * @since 0.1.0
     *
     * @param string ...$stopSequences The stop sequences.
     * @return self
     */
    public function usingStopSequences(string ...$stopSequences): self
    {
        $this->modelConfig->setStopSequences($stopSequences);
        return $this;
    }
    /**
     * Sets the number of candidates to generate.
     *
     * @since 0.1.0
     *
     * @param int $candidateCount The number of candidates.
     * @return self
     */
    public function usingCandidateCount(int $candidateCount): self
    {
        $this->modelConfig->setCandidateCount($candidateCount);
        return $this;
    }
    /**
     * Sets the function declarations available to the model.
     *
     * @since 0.1.0
     *
     * @param FunctionDeclaration ...$functionDeclarations The function declarations.
     * @return self
     */
    public function usingFunctionDeclarations(FunctionDeclaration ...$functionDeclarations): self
    {
        $this->modelConfig->setFunctionDeclarations($functionDeclarations);
        return $this;
    }
    /**
     * Sets the presence penalty for generation.
     *
     * @since 0.1.0
     *
     * @param float $presencePenalty The presence penalty value.
     * @return self
     */
    public function usingPresencePenalty(float $presencePenalty): self
    {
        $this->modelConfig->setPresencePenalty($presencePenalty);
        return $this;
    }
    /**
     * Sets the frequency penalty for generation.
     *
     * @since 0.1.0
     *
     * @param float $frequencyPenalty The frequency penalty value.
     * @return self
     */
    public function usingFrequencyPenalty(float $frequencyPenalty): self
    {
        $this->modelConfig->setFrequencyPenalty($frequencyPenalty);
        return $this;
    }
    /**
     * Sets the web search configuration.
     *
     * @since 0.1.0
     *
     * @param WebSearch $webSearch The web search configuration.
     * @return self
     */
    public function usingWebSearch(WebSearch $webSearch): self
    {
        $this->modelConfig->setWebSearch($webSearch);
        return $this;
    }
    /**
     * Sets the request options for HTTP transport.
     *
     * @since 0.3.0
     *
     * @param RequestOptions $requestOptions The request options.
     * @return self
     */
    public function usingRequestOptions(RequestOptions $requestOptions): self
    {
        $this->requestOptions = $requestOptions;
        return $this;
    }
    /**
     * Sets the top log probabilities configuration.
     *
     * If $topLogprobs is null, enables log probabilities.
     * If $topLogprobs has a value, enables log probabilities and sets the number of top log probabilities to return.
     *
     * @since 0.1.0
     *
     * @param int|null $topLogprobs The number of top log probabilities to return, or null to enable log probabilities.
     * @return self
     */
    public function usingTopLogprobs(?int $topLogprobs = null): self
    {
        // Always enable log probabilities
        $this->modelConfig->setLogprobs(\true);
        // If a specific number is provided, set it
        if ($topLogprobs !== null) {
            $this->modelConfig->setTopLogprobs($topLogprobs);
        }
        return $this;
    }
    /**
     * Sets the output MIME type.
     *
     * @since 0.1.0
     *
     * @param string $mimeType The MIME type.
     * @return self
     */
    public function asOutputMimeType(string $mimeType): self
    {
        $this->modelConfig->setOutputMimeType($mimeType);
        return $this;
    }
    /**
     * Sets the output schema.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed> $schema The output schema.
     * @return self
     */
    public function asOutputSchema(array $schema): self
    {
        $this->modelConfig->setOutputSchema($schema);
        return $this;
    }
    /**
     * Sets the output modalities.
     *
     * @since 0.1.0
     *
     * @param ModalityEnum ...$modalities The output modalities.
     * @return self
     */
    public function asOutputModalities(ModalityEnum ...$modalities): self
    {
        $this->modelConfig->setOutputModalities($modalities);
        return $this;
    }
    /**
     * Sets the output file type.
     *
     * @since 0.1.0
     *
     * @param FileTypeEnum $fileType The output file type.
     * @return self
     */
    public function asOutputFileType(FileTypeEnum $fileType): self
    {
        $this->modelConfig->setOutputFileType($fileType);
        return $this;
    }
    /**
     * Sets the output media orientation.
     *
     * @since 1.3.0
     *
     * @param MediaOrientationEnum $orientation The output media orientation.
     * @return self
     */
    public function asOutputMediaOrientation(MediaOrientationEnum $orientation): self
    {
        $this->modelConfig->setOutputMediaOrientation($orientation);
        return $this;
    }
    /**
     * Sets the output media aspect ratio.
     *
     * If set, this supersedes the output media orientation, as it is a more
     * specific configuration.
     *
     * @since 1.3.0
     *
     * @param string $aspectRatio The aspect ratio (e.g. "16:9", "3:2").
     * @return self
     */
    public function asOutputMediaAspectRatio(string $aspectRatio): self
    {
        $this->modelConfig->setOutputMediaAspectRatio($aspectRatio);
        return $this;
    }
    /**
     * Sets the output speech voice.
     *
     * @since 1.3.0
     *
     * @param string $voice The output speech voice.
     * @return self
     */
    public function asOutputSpeechVoice(string $voice): self
    {
        $this->modelConfig->setOutputSpeechVoice($voice);
        return $this;
    }
    /**
     * Configures the prompt for JSON response output.
     *
     * @since 0.1.0
     *
     * @param array<string, mixed>|null $schema Optional JSON schema.
     * @return self
     */
    public function asJsonResponse(?array $schema = null): self
    {
        $this->asOutputMimeType('application/json');
        if ($schema !== null) {
            $this->asOutputSchema($schema);
        }
        return $this;
    }
    /**
     * Infers the capability from configured output modalities.
     *
     * @since 0.1.0
     *
     * @return CapabilityEnum The inferred capability.
     * @throws RuntimeException If the output modality is not supported.
     */
    private function inferCapabilityFromOutputModalities(): CapabilityEnum
    {
        // Get the configured output modalities
        $outputModalities = $this->modelConfig->getOutputModalities();
        // Default to text if no output modality is specified
        if ($outputModalities === null || empty($outputModalities)) {
            return CapabilityEnum::textGeneration();
        }
        // Multi-modal output (multiple modalities) defaults to text generation. This is temporary
        // as a multi-modal interface will be implemented in the future.
        if (count($outputModalities) > 1) {
            return CapabilityEnum::textGeneration();
        }
        // Infer capability from single output modality
        $outputModality = $outputModalities[0];
        if ($outputModality->isText()) {
            return CapabilityEnum::textGeneration();
        } elseif ($outputModality->isImage()) {
            return CapabilityEnum::imageGeneration();
        } elseif ($outputModality->isAudio()) {
            return CapabilityEnum::speechGeneration();
        } elseif ($outputModality->isVideo()) {
            return CapabilityEnum::videoGeneration();
        } else {
            // For unsupported modalities, provide a clear error message
            throw new RuntimeException(sprintf('Output modality "%s" is not yet supported.', $outputModality->value));
        }
    }
    /**
     * Infers the capability from a model's implemented interfaces.
     *
     * @since 0.1.0
     *
     * @param ModelInterface $model The model to infer capability from.
     * @return CapabilityEnum|null The inferred capability, or null if none can be inferred.
     */
    private function inferCapabilityFromModelInterfaces(ModelInterface $model): ?CapabilityEnum
    {
        // Check model interfaces in order of preference
        if ($model instanceof TextGenerationModelInterface) {
            return CapabilityEnum::textGeneration();
        }
        if ($model instanceof ImageGenerationModelInterface) {
            return CapabilityEnum::imageGeneration();
        }
        if ($model instanceof TextToSpeechConversionModelInterface) {
            return CapabilityEnum::textToSpeechConversion();
        }
        if ($model instanceof SpeechGenerationModelInterface) {
            return CapabilityEnum::speechGeneration();
        }
        if ($model instanceof VideoGenerationModelInterface) {
            return CapabilityEnum::videoGeneration();
        }
        // No supported interface found
        return null;
    }
    /**
     * Checks if the current prompt is supported by the selected model.
     *
     * @since 0.1.0
     * @since 0.3.0 Method visibility changed to public.
     *
     * @param CapabilityEnum|null $capability Optional capability to check support for.
     * @return bool True if supported, false otherwise.
     */
    public function isSupported(?CapabilityEnum $capability = null): bool
    {
        // If no intended capability provided, infer from output modalities
        if ($capability === null) {
            // First try to infer from a specific model if one is set
            if ($this->model !== null) {
                $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
                if ($inferredCapability !== null) {
                    $capability = $inferredCapability;
                }
            }
            // If still no capability, infer from output modalities
            if ($capability === null) {
                $capability = $this->inferCapabilityFromOutputModalities();
            }
        }
        // Build requirements with the specified capability
        $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
        // If the model has been set, check if it meets the requirements
        if ($this->model !== null) {
            return $requirements->areMetBy($this->model->metadata());
        }
        try {
            // Check if any models support these requirements
            $models = $this->registry->findModelsMetadataForSupport($requirements);
            return !empty($models);
        } catch (InvalidArgumentException $e) {
            // No models support the requirements
            return \false;
        }
    }
    /**
     * Checks if the prompt is supported for text generation.
     *
     * @since 0.1.0
     *
     * @return bool True if text generation is supported.
     */
    public function isSupportedForTextGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::textGeneration());
    }
    /**
     * Checks if the prompt is supported for image generation.
     *
     * @since 0.1.0
     *
     * @return bool True if image generation is supported.
     */
    public function isSupportedForImageGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::imageGeneration());
    }
    /**
     * Checks if the prompt is supported for text to speech conversion.
     *
     * @since 0.1.0
     *
     * @return bool True if text to speech conversion is supported.
     */
    public function isSupportedForTextToSpeechConversion(): bool
    {
        return $this->isSupported(CapabilityEnum::textToSpeechConversion());
    }
    /**
     * Checks if the prompt is supported for video generation.
     *
     * @since 0.1.0
     *
     * @return bool True if video generation is supported.
     */
    public function isSupportedForVideoGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::videoGeneration());
    }
    /**
     * Checks if the prompt is supported for speech generation.
     *
     * @since 0.1.0
     *
     * @return bool True if speech generation is supported.
     */
    public function isSupportedForSpeechGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::speechGeneration());
    }
    /**
     * Checks if the prompt is supported for music generation.
     *
     * @since 0.1.0
     *
     * @return bool True if music generation is supported.
     */
    public function isSupportedForMusicGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::musicGeneration());
    }
    /**
     * Checks if the prompt is supported for embedding generation.
     *
     * @since 0.1.0
     *
     * @return bool True if embedding generation is supported.
     */
    public function isSupportedForEmbeddingGeneration(): bool
    {
        return $this->isSupported(CapabilityEnum::embeddingGeneration());
    }
    /**
     * Generates a result from the prompt.
     *
     * This is the primary execution method that generates a result (containing
     * potentially multiple candidates) based on the specified capability or
     * the configured output modality.
     *
     * @since 0.1.0
     *
     * @param CapabilityEnum|null $capability Optional capability to use for generation.
     *                                        If null, capability is inferred from output modality.
     * @return GenerativeAiResult The generated result containing candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support the required capability.
     */
    public function generateResult(?CapabilityEnum $capability = null): GenerativeAiResult
    {
        $this->validateMessages();
        // If capability is not provided, infer it
        if ($capability === null) {
            // First try to infer from a specific model if one is set
            if ($this->model !== null) {
                $inferredCapability = $this->inferCapabilityFromModelInterfaces($this->model);
                if ($inferredCapability !== null) {
                    $capability = $inferredCapability;
                }
            }
            // If still no capability, infer from output modalities
            if ($capability === null) {
                $capability = $this->inferCapabilityFromOutputModalities();
            }
        }
        $model = $this->getConfiguredModel($capability);
        // Dispatch BeforeGenerateResultEvent
        $this->dispatchEvent(new BeforeGenerateResultEvent($this->messages, $model, $capability));
        // Route to the appropriate generation method based on capability
        $result = $this->executeModelGeneration($model, $capability, $this->messages);
        // Dispatch AfterGenerateResultEvent
        $this->dispatchEvent(new AfterGenerateResultEvent($this->messages, $model, $capability, $result));
        return $result;
    }
    /**
     * Executes the model generation based on capability.
     *
     * @since 0.4.0
     *
     * @param ModelInterface $model The model to use for generation.
     * @param CapabilityEnum $capability The capability to use.
     * @param list<Message> $messages The messages to send.
     * @return GenerativeAiResult The generated result.
     * @throws RuntimeException If the model doesn't support the required capability.
     */
    private function executeModelGeneration(ModelInterface $model, CapabilityEnum $capability, array $messages): GenerativeAiResult
    {
        if ($capability->isTextGeneration()) {
            if (!$model instanceof TextGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support text generation.', $model->metadata()->getId()));
            }
            return $model->generateTextResult($messages);
        }
        if ($capability->isImageGeneration()) {
            if (!$model instanceof ImageGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support image generation.', $model->metadata()->getId()));
            }
            return $model->generateImageResult($messages);
        }
        if ($capability->isTextToSpeechConversion()) {
            if (!$model instanceof TextToSpeechConversionModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support text-to-speech conversion.', $model->metadata()->getId()));
            }
            return $model->convertTextToSpeechResult($messages);
        }
        if ($capability->isSpeechGeneration()) {
            if (!$model instanceof SpeechGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support speech generation.', $model->metadata()->getId()));
            }
            return $model->generateSpeechResult($messages);
        }
        if ($capability->isVideoGeneration()) {
            if (!$model instanceof VideoGenerationModelInterface) {
                throw new RuntimeException(sprintf('Model "%s" does not support video generation.', $model->metadata()->getId()));
            }
            return $model->generateVideoResult($messages);
        }
        // TODO: Add support for other capabilities when interfaces are available
        throw new RuntimeException(sprintf('Capability "%s" is not yet supported for generation.', $capability->value));
    }
    /**
     * Generates a text result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing text candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support text generation.
     */
    public function generateTextResult(): GenerativeAiResult
    {
        // Include text in output modalities
        $this->includeOutputModalities(ModalityEnum::text());
        // Generate and return the result with text generation capability
        return $this->generateResult(CapabilityEnum::textGeneration());
    }
    /**
     * Generates an image result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing image candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support image generation.
     */
    public function generateImageResult(): GenerativeAiResult
    {
        // Include image in output modalities
        $this->includeOutputModalities(ModalityEnum::image());
        // Generate and return the result with image generation capability
        return $this->generateResult(CapabilityEnum::imageGeneration());
    }
    /**
     * Generates a speech result from the prompt.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing speech audio candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support speech generation.
     */
    public function generateSpeechResult(): GenerativeAiResult
    {
        // Include audio in output modalities
        $this->includeOutputModalities(ModalityEnum::audio());
        // Generate and return the result with speech generation capability
        return $this->generateResult(CapabilityEnum::speechGeneration());
    }
    /**
     * Converts text to speech and returns the result.
     *
     * @since 0.1.0
     *
     * @return GenerativeAiResult The generated result containing speech audio candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support text-to-speech conversion.
     */
    public function convertTextToSpeechResult(): GenerativeAiResult
    {
        // Include audio in output modalities
        $this->includeOutputModalities(ModalityEnum::audio());
        // Generate and return the result with text-to-speech conversion capability
        return $this->generateResult(CapabilityEnum::textToSpeechConversion());
    }
    /**
     * Generates a video result from the prompt.
     *
     * @since 1.3.0
     *
     * @return GenerativeAiResult The generated result containing video candidates.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If the model doesn't support video generation.
     */
    public function generateVideoResult(): GenerativeAiResult
    {
        // Include video in output modalities
        $this->includeOutputModalities(ModalityEnum::video());
        // Generate and return the result with video generation capability
        return $this->generateResult(CapabilityEnum::videoGeneration());
    }
    /**
     * Generates text from the prompt.
     *
     * @since 0.1.0
     *
     * @return string The generated text.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     */
    public function generateText(): string
    {
        return $this->generateTextResult()->toText();
    }
    /**
     * Generates multiple text candidates from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of candidates to generate.
     * @return list<string> The generated texts.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     */
    public function generateTexts(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        // Generate text result
        return $this->generateTextResult()->toTexts();
    }
    /**
     * Generates an image from the prompt.
     *
     * @since 0.1.0
     *
     * @return File The generated image file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no image is generated.
     */
    public function generateImage(): File
    {
        return $this->generateImageResult()->toFile();
    }
    /**
     * Generates multiple images from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of images to generate.
     * @return list<File> The generated image files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no images are generated.
     */
    public function generateImages(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateImageResult()->toFiles();
    }
    /**
     * Converts text to speech.
     *
     * @since 0.1.0
     *
     * @return File The generated speech audio file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function convertTextToSpeech(): File
    {
        return $this->convertTextToSpeechResult()->toFile();
    }
    /**
     * Converts text to multiple speech outputs.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of speech outputs to generate.
     * @return list<File> The generated speech audio files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function convertTextToSpeeches(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->convertTextToSpeechResult()->toFiles();
    }
    /**
     * Generates speech from the prompt.
     *
     * @since 0.1.0
     *
     * @return File The generated speech audio file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function generateSpeech(): File
    {
        return $this->generateSpeechResult()->toFile();
    }
    /**
     * Generates multiple speech outputs from the prompt.
     *
     * @since 0.1.0
     *
     * @param int|null $candidateCount The number of speech outputs to generate.
     * @return list<File> The generated speech audio files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no audio is generated.
     */
    public function generateSpeeches(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateSpeechResult()->toFiles();
    }
    /**
     * Generates a video from the prompt.
     *
     * @since 1.3.0
     *
     * @return File The generated video file.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no video is generated.
     */
    public function generateVideo(): File
    {
        return $this->generateVideoResult()->toFile();
    }
    /**
     * Generates multiple videos from the prompt.
     *
     * @since 1.3.0
     *
     * @param int|null $candidateCount The number of videos to generate.
     * @return list<File> The generated video files.
     * @throws InvalidArgumentException If the prompt or model validation fails.
     * @throws RuntimeException If no videos are generated.
     */
    public function generateVideos(?int $candidateCount = null): array
    {
        if ($candidateCount !== null) {
            $this->usingCandidateCount($candidateCount);
        }
        return $this->generateVideoResult()->toFiles();
    }
    /**
     * Appends a MessagePart to the messages array.
     *
     * If the last message has a user role, the part is added to it.
     * Otherwise, a new UserMessage is created with the part.
     *
     * @since 0.1.0
     *
     * @param MessagePart $part The part to append.
     * @return void
     */
    protected function appendPartToMessages(MessagePart $part): void
    {
        $lastMessage = end($this->messages);
        if ($lastMessage instanceof Message && $lastMessage->getRole()->isUser()) {
            // Replace the last message with a new one containing the appended part
            array_pop($this->messages);
            $this->messages[] = $lastMessage->withPart($part);
            return;
        }
        // Create new UserMessage with the part
        $this->messages[] = new UserMessage([$part]);
    }
    /**
     * Gets the model to use for generation.
     *
     * If a model has been explicitly set, validates it meets requirements and returns it.
     * Otherwise, finds a suitable model based on the prompt requirements.
     *
     * @since 0.1.0
     *
     * @param CapabilityEnum $capability The capability the model will be using.
     * @return ModelInterface The model to use.
     * @throws InvalidArgumentException If no suitable model is found or set model doesn't meet requirements.
     */
    private function getConfiguredModel(CapabilityEnum $capability): ModelInterface
    {
        $requirements = ModelRequirements::fromPromptData($capability, $this->messages, $this->modelConfig);
        if ($this->model !== null) {
            // Explicit model was provided via usingModel(); just update config and bind dependencies.
            $model = $this->model;
            $model->setConfig($this->modelConfig);
            $this->registry->bindModelDependencies($model);
            $this->bindModelRequestOptions($model);
            return $model;
        }
        // Retrieve the candidate models map which satisfies the requirements.
        $candidateMap = $this->getCandidateModelsMap($requirements);
        if (empty($candidateMap)) {
            $message = sprintf('No models found that support %s for this prompt.', $capability->value);
            if ($this->providerIdOrClassName !== null) {
                $message = sprintf('No models found for provider "%s" that support %s for this prompt.', $this->providerIdOrClassName, $capability->value);
            }
            throw new InvalidArgumentException($message);
        }
        // Check if any preferred models match the candidates, in priority order.
        if (!empty($this->modelPreferenceKeys)) {
            // Find preferences that match available candidates, preserving preference order.
            $matchingPreferences = array_intersect_key(array_flip($this->modelPreferenceKeys), $candidateMap);
            if (!empty($matchingPreferences)) {
                // Get the first matching preference key
                $firstMatchKey = key($matchingPreferences);
                [$providerId, $modelId] = $candidateMap[$firstMatchKey];
                $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
                $this->bindModelRequestOptions($model);
                return $model;
            }
        }
        // No preference matched; fall back to the first candidate discovered.
        [$providerId, $modelId] = reset($candidateMap);
        $model = $this->registry->getProviderModel($providerId, $modelId, $this->modelConfig);
        $this->bindModelRequestOptions($model);
        return $model;
    }
    /**
     * Binds configured request options to the model if present and supported.
     *
     * Request options are only applicable to API-based models that make HTTP requests.
     *
     * @since 0.3.0
     *
     * @param ModelInterface $model The model to bind request options to.
     * @return void
     */
    private function bindModelRequestOptions(ModelInterface $model): void
    {
        if ($this->requestOptions !== null && $model instanceof ApiBasedModelInterface) {
            $model->setRequestOptions($this->requestOptions);
        }
    }
    /**
     * Builds a map of candidate models that satisfy the requirements for efficient lookup.
     *
     * @since 0.2.0
     *
     * @param ModelRequirements $requirements The requirements derived from the prompt.
     * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
     */
    private function getCandidateModelsMap(ModelRequirements $requirements): array
    {
        if ($this->providerIdOrClassName === null) {
            // No provider locked in, gather all models across providers that meet requirements.
            $providerModelsMetadata = $this->registry->findModelsMetadataForSupport($requirements);
            $candidateMap = [];
            foreach ($providerModelsMetadata as $providerModels) {
                $providerId = $providerModels->getProvider()->getId();
                $providerMap = $this->generateMapFromCandidates($providerId, $providerModels->getModels());
                // Use + operator to merge, preserving keys from $candidateMap (first provider wins for model-only keys)
                $candidateMap = $candidateMap + $providerMap;
            }
            return $candidateMap;
        }
        // Provider set, only consider models from that provider.
        $modelsMetadata = $this->registry->findProviderModelsMetadataForSupport($this->providerIdOrClassName, $requirements);
        // Ensure we pass the provider ID, not the class name
        $providerId = $this->registry->getProviderId($this->providerIdOrClassName);
        return $this->generateMapFromCandidates($providerId, $modelsMetadata);
    }
    /**
     * Generates a candidate map from model metadata with both provider-specific and model-only keys.
     *
     * @since 0.2.0
     *
     * @param string $providerId The provider ID.
     * @param list<ModelMetadata> $modelsMetadata The models metadata to map.
     * @return array<string, array{0:string,1:string}> Map of preference keys to [providerId, modelId] tuples.
     */
    private function generateMapFromCandidates(string $providerId, array $modelsMetadata): array
    {
        $map = [];
        foreach ($modelsMetadata as $modelMetadata) {
            $modelId = $modelMetadata->getId();
            // Add provider-specific key
            $providerModelKey = $this->createProviderModelPreferenceKey($providerId, $modelId);
            $map[$providerModelKey] = [$providerId, $modelId];
            // Add model-only key
            $modelKey = $this->createModelPreferenceKey($modelId);
            $map[$modelKey] = [$providerId, $modelId];
        }
        return $map;
    }
    /**
     * Normalizes and validates a preference identifier string.
     *
     * @since 0.2.0
     *
     * @param mixed $value The value to normalize.
     * @param string $emptyMessage The message for empty or invalid values.
     * @return string The normalized identifier.
     *
     * @throws InvalidArgumentException If the value is not a non-empty string.
     */
    private function normalizePreferenceIdentifier($value, string $emptyMessage = 'Model preference identifiers cannot be empty.'): string
    {
        if (!is_string($value)) {
            throw new InvalidArgumentException($emptyMessage);
        }
        $trimmed = trim($value);
        if ($trimmed === '') {
            throw new InvalidArgumentException($emptyMessage);
        }
        return $trimmed;
    }
    /**
     * Creates a preference key for a provider/model combination.
     *
     * @since 0.2.0
     *
     * @param string $providerId The provider identifier.
     * @param string $modelId The model identifier.
     * @return string The generated preference key.
     */
    private function createProviderModelPreferenceKey(string $providerId, string $modelId): string
    {
        return 'providerModel::' . $providerId . '::' . $modelId;
    }
    /**
     * Creates a preference key for a model identifier.
     *
     * @since 0.2.0
     *
     * @param string $modelId The model identifier.
     * @return string The generated preference key.
     */
    private function createModelPreferenceKey(string $modelId): string
    {
        return 'model::' . $modelId;
    }
    /**
     * Parses various input types into a Message with the given role.
     *
     * @since 0.1.0
     *
     * @param mixed $input The input to parse.
     * @param MessageRoleEnum $defaultRole The role for the message if not specified by input.
     * @return Message The parsed message.
     * @throws InvalidArgumentException If the input type is not supported or results in empty message.
     */
    private function parseMessage($input, MessageRoleEnum $defaultRole): Message
    {
        // Handle Message input directly
        if ($input instanceof Message) {
            return $input;
        }
        // Handle single MessagePart
        if ($input instanceof MessagePart) {
            return new Message($defaultRole, [$input]);
        }
        // Handle string input
        if (is_string($input)) {
            if (trim($input) === '') {
                throw new InvalidArgumentException('Cannot create a message from an empty string.');
            }
            return new Message($defaultRole, [new MessagePart($input)]);
        }
        // Handle array input
        if (!is_array($input)) {
            throw new InvalidArgumentException('Input must be a string, MessagePart, MessagePartArrayShape, ' . 'a list of string|MessagePart|MessagePartArrayShape, or a Message instance.');
        }
        // Handle MessageArrayShape input
        if (Message::isArrayShape($input)) {
            return Message::fromArray($input);
        }
        // Check if it's a MessagePartArrayShape
        if (MessagePart::isArrayShape($input)) {
            return new Message($defaultRole, [MessagePart::fromArray($input)]);
        }
        // It should be a list of string|MessagePart|MessagePartArrayShape
        if (!array_is_list($input)) {
            throw new InvalidArgumentException('Array input must be a list array.');
        }
        // Empty array check
        if (empty($input)) {
            throw new InvalidArgumentException('Cannot create a message from an empty array.');
        }
        $parts = [];
        foreach ($input as $item) {
            if (is_string($item)) {
                $parts[] = new MessagePart($item);
            } elseif ($item instanceof MessagePart) {
                $parts[] = $item;
            } elseif (is_array($item) && MessagePart::isArrayShape($item)) {
                $parts[] = MessagePart::fromArray($item);
            } else {
                throw new InvalidArgumentException('Array items must be strings, MessagePart instances, or MessagePartArrayShape.');
            }
        }
        return new Message($defaultRole, $parts);
    }
    /**
     * Validates the messages array for prompt generation.
     *
     * Ensures that:
     * - The first message is a user message
     * - The last message is a user message
     * - The last message has parts
     *
     * @since 0.1.0
     *
     * @return void
     * @throws InvalidArgumentException If validation fails.
     */
    private function validateMessages(): void
    {
        if (empty($this->messages)) {
            throw new InvalidArgumentException('Cannot generate from an empty prompt. Add content using withText() or similar methods.');
        }
        $firstMessage = reset($this->messages);
        if (!$firstMessage->getRole()->isUser()) {
            throw new InvalidArgumentException('The first message must be from a user role, not from ' . $firstMessage->getRole()->value);
        }
        $lastMessage = end($this->messages);
        if (!$lastMessage->getRole()->isUser()) {
            throw new InvalidArgumentException('The last message must be from a user role, not from ' . $lastMessage->getRole()->value);
        }
        if (empty($lastMessage->getParts())) {
            throw new InvalidArgumentException('The last message must have content parts. Add content using withText() or similar methods.');
        }
    }
    /**
     * Checks if the value is a list of Message objects.
     *
     * @since 0.1.0
     *
     * @param mixed $value The value to check.
     * @return bool True if the value is a list of Message objects.
     *
     * @phpstan-assert-if-true list<Message> $value
     */
    private function isMessagesList($value): bool
    {
        if (!is_array($value) || empty($value) || !array_is_list($value)) {
            return \false;
        }
        // Check if all items are Messages
        foreach ($value as $item) {
            if (!$item instanceof Message) {
                return \false;
            }
        }
        return \true;
    }
    /**
     * Includes output modalities if not already present.
     *
     * Adds the given modalities to the output modalities list if they're not
     * already included. If output modalities is null, initializes it with
     * the given modalities.
     *
     * @since 0.1.0
     *
     * @param ModalityEnum ...$modalities The modalities to include.
     * @return void
     */
    private function includeOutputModalities(ModalityEnum ...$modalities): void
    {
        $existing = $this->modelConfig->getOutputModalities();
        // Initialize if null
        if ($existing === null) {
            $this->modelConfig->setOutputModalities($modalities);
            return;
        }
        // Build a set of existing modality values for O(1) lookup
        $existingValues = [];
        foreach ($existing as $existingModality) {
            $existingValues[$existingModality->value] = \true;
        }
        // Add new modalities that don't exist
        $toAdd = [];
        foreach ($modalities as $modality) {
            if (!isset($existingValues[$modality->value])) {
                $toAdd[] = $modality;
            }
        }
        // Update if we have new modalities to add
        if (!empty($toAdd)) {
            $this->modelConfig->setOutputModalities(array_merge($existing, $toAdd));
        }
    }
    /**
     * Dispatches an event if an event dispatcher is registered.
     *
     * @since 0.4.0
     *
     * @param object $event The event to dispatch.
     * @return void
     */
    private function dispatchEvent(object $event): void
    {
        if ($this->eventDispatcher !== null) {
            $this->eventDispatcher->dispatch($event);
        }
    }
}
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             