[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/php-ai-client/src/Files/DTO/ -> File.php (source)

   1  <?php
   2  
   3  declare (strict_types=1);
   4  namespace WordPress\AiClient\Files\DTO;
   5  
   6  use WordPress\AiClient\Common\AbstractDataTransferObject;
   7  use WordPress\AiClient\Common\Exception\InvalidArgumentException;
   8  use WordPress\AiClient\Common\Exception\RuntimeException;
   9  use WordPress\AiClient\Files\Enums\FileTypeEnum;
  10  use WordPress\AiClient\Files\ValueObjects\MimeType;
  11  /**
  12   * Represents a file in the AI client.
  13   *
  14   * A file is either remote (an http(s) URL that the provider fetches) or inline (base64-encoded data).
  15   *
  16   * The constructor accepts an http(s) URL, a data URI, or plain base64 data with a MIME type. For backward
  17   * compatibility it still detects an existing local file path and reads it, but this is deprecated and will be
  18   * removed in the next major version: load local files explicitly via {@see File::fromLocalPath()} instead.
  19   * The named constructors {@see File::fromUrl()}, {@see File::fromDataUri()} and {@see File::fromBase64()}
  20   * never access the filesystem and should be preferred whenever the kind of input is known, especially for
  21   * data that comes from an untrusted source such as an API response or user input. {@see File::fromArray()}
  22   * never accesses the filesystem.
  23   *
  24   * @since 0.1.0
  25   * @since 1.5.1 Implicit local file path detection in the constructor is deprecated; non-http(s) URL schemes
  26   *                are rejected before any filesystem access.
  27   *
  28   * @phpstan-type FileArrayShape array{
  29   *     fileType: string,
  30   *     url?: string,
  31   *     mimeType: string,
  32   *     base64Data?: string
  33   * }
  34   *
  35   * @extends AbstractDataTransferObject<FileArrayShape>
  36   */
  37  class File extends AbstractDataTransferObject
  38  {
  39      public const KEY_FILE_TYPE = 'fileType';
  40      public const KEY_MIME_TYPE = 'mimeType';
  41      public const KEY_URL = 'url';
  42      public const KEY_BASE64_DATA = 'base64Data';
  43      /**
  44       * Pattern matching a data URI with base64 payload.
  45       *
  46       * Group 1 is the optional MIME type (with parameters), group 2 is the base64 payload.
  47       *
  48       * @since 1.5.1
  49       */
  50      private const DATA_URI_PATTERN = '/^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})\z/';
  51      /**
  52       * Pattern matching plain base64 data (an empty payload is allowed, e.g. for an empty file).
  53       *
  54       * @since 1.5.1
  55       */
  56      private const BASE64_PATTERN = '/^[A-Za-z0-9+\/]*={0,2}\z/';
  57      /**
  58       * @var MimeType The MIME type of the file.
  59       */
  60      private MimeType $mimeType;
  61      /**
  62       * @var FileTypeEnum The type of file storage.
  63       */
  64      private FileTypeEnum $fileType;
  65      /**
  66       * @var string|null The URL for remote files.
  67       */
  68      private ?string $url = null;
  69      /**
  70       * @var string|null The base64 data for inline files.
  71       */
  72      private ?string $base64Data = null;
  73      /**
  74       * Constructor.
  75       *
  76       * Passing a local file path is deprecated; use {@see File::fromLocalPath()} instead. When a local path is
  77       * detected, an E_USER_DEPRECATED notice is emitted and the file is read.
  78       *
  79       * @since 0.1.0
  80       * @since 1.5.1 Local file path detection is deprecated. Non-http(s) URL schemes are rejected.
  81       *
  82       * @param string $file The file string (http(s) URL, data URI, plain base64 data, or deprecated local path).
  83       * @param string|null $mimeType The MIME type of the file (optional, required for plain base64 data).
  84       * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
  85       */
  86      public function __construct(string $file, ?string $mimeType = null)
  87      {
  88          $this->detectAndProcessFile($file, $mimeType);
  89      }
  90      /**
  91       * Creates a remote file from an http(s) URL.
  92       *
  93       * The URL is not fetched by the client; it is passed on to the provider.
  94       *
  95       * @since 1.5.1
  96       *
  97       * @param string $url The http(s) URL.
  98       * @param string|null $mimeType The MIME type (optional, otherwise inferred from the URL path extension).
  99       * @return self The remote file.
 100       * @throws InvalidArgumentException If the URL is not an http(s) URL or the MIME type cannot be determined.
 101       */
 102      public static function fromUrl(string $url, ?string $mimeType = null): self
 103      {
 104          if (!self::isHttpUrl($url)) {
 105              throw new InvalidArgumentException('Invalid URL. Only http:// and https:// URLs are supported.');
 106          }
 107          return new self($url, $mimeType);
 108      }
 109      /**
 110       * Creates an inline file from a data URI (data:[mime/type];base64,[data]).
 111       *
 112       * @since 1.5.1
 113       *
 114       * @param string $dataUri The data URI.
 115       * @param string|null $mimeType The MIME type (optional, overrides the MIME type embedded in the data URI).
 116       * @return self The inline file.
 117       * @throws InvalidArgumentException If the data URI is invalid or the MIME type cannot be determined.
 118       */
 119      public static function fromDataUri(string $dataUri, ?string $mimeType = null): self
 120      {
 121          if (!preg_match(self::DATA_URI_PATTERN, $dataUri)) {
 122              throw new InvalidArgumentException('Invalid data URI. Expected the format data:[mime/type];base64,[data].');
 123          }
 124          return new self($dataUri, $mimeType);
 125      }
 126      /**
 127       * Creates an inline file from plain base64-encoded data.
 128       *
 129       * @since 1.5.1
 130       *
 131       * @param string $base64 The base64-encoded data (without data URI prefix).
 132       * @param string $mimeType The MIME type of the data.
 133       * @return self The inline file.
 134       * @throws InvalidArgumentException If the data is not valid base64 or the MIME type is invalid.
 135       */
 136      public static function fromBase64(string $base64, string $mimeType): self
 137      {
 138          if (str_starts_with($base64, 'data:')) {
 139              throw new InvalidArgumentException('Expected raw base64 data; use File::fromDataUri() for data URIs.');
 140          }
 141          if (!self::isBase64($base64)) {
 142              throw new InvalidArgumentException('Invalid base64 data provided.');
 143          }
 144          if (!MimeType::isValid($mimeType)) {
 145              throw new InvalidArgumentException(sprintf('Invalid MIME type: %s', $mimeType));
 146          }
 147          /*
 148           * Build an empty inline file through the data URI path (deterministic, filesystem-free), then attach the
 149           * payload directly. Embedding the payload in a data URI and parsing it again would create several
 150           * additional copies of a potentially large string.
 151           */
 152          $instance = new self(sprintf('data:%s;base64,', $mimeType));
 153          $instance->base64Data = $base64;
 154          return $instance;
 155      }
 156      /**
 157       * Creates an inline file by reading a file from the local filesystem.
 158       *
 159       * Never pass untrusted input (such as user input or API response data) to this method.
 160       *
 161       * @since 1.5.1
 162       *
 163       * @param string $path The path to the local file. Stream wrappers (e.g. ftp://, phar://) are not supported.
 164       * @param string|null $mimeType The MIME type (optional, otherwise inferred from the file extension).
 165       * @return self The inline file.
 166       * @throws InvalidArgumentException If the path is empty, uses a stream wrapper, is not a regular file,
 167       *                                  or the MIME type cannot be determined.
 168       * @throws RuntimeException If the file cannot be read.
 169       */
 170      public static function fromLocalPath(string $path, ?string $mimeType = null): self
 171      {
 172          if ($path === '') {
 173              throw new InvalidArgumentException('Local file path must not be empty.');
 174          }
 175          // Reject stream wrappers before touching the filesystem, so no wrapper can open a connection.
 176          if (str_contains($path, '://')) {
 177              throw new InvalidArgumentException('Stream wrappers are not supported in local file paths.');
 178          }
 179          if (!is_file($path)) {
 180              throw new InvalidArgumentException(sprintf('Local file not found or not a regular file: %s', $path));
 181          }
 182          $resolvedMimeType = $mimeType !== null ? new MimeType($mimeType) : self::determineMimeType(null, null, $path);
 183          $fileContent = @file_get_contents($path);
 184          if ($fileContent === \false) {
 185              throw new RuntimeException(sprintf('Unable to read file: %s', $path));
 186          }
 187          return self::fromBase64(base64_encode($fileContent), (string) $resolvedMimeType);
 188      }
 189      /**
 190       * Detects the file type and processes it accordingly.
 191       *
 192       * @since 0.1.0
 193       * @since 1.5.1 Rejects non-http(s) URL schemes before any filesystem access; local path detection deprecated.
 194       *
 195       * @param string $file The file string to process.
 196       * @param string|null $providedMimeType The explicitly provided MIME type.
 197       * @throws InvalidArgumentException If the file format is invalid or MIME type cannot be determined.
 198       */
 199      private function detectAndProcessFile(string $file, ?string $providedMimeType): void
 200      {
 201          // Check if it's an http(s) URL.
 202          if (self::isHttpUrl($file)) {
 203              $this->fileType = FileTypeEnum::remote();
 204              $this->url = $file;
 205              $this->mimeType = self::determineMimeType($providedMimeType, null, $file);
 206              return;
 207          }
 208          // Any other URL scheme (file://, ftp://, phar://, ...) is rejected before anything else runs.
 209          if (str_contains($file, '://')) {
 210              throw new InvalidArgumentException('Unsupported URL scheme. Only http:// and https:// URLs are supported.');
 211          }
 212          // Check if it's a data URI.
 213          if (preg_match(self::DATA_URI_PATTERN, $file, $matches)) {
 214              $this->fileType = FileTypeEnum::inline();
 215              $this->base64Data = $matches[2];
 216              // Extract just the base64 data
 217              $extractedMimeType = empty($matches[1]) ? null : $matches[1];
 218              $this->mimeType = self::determineMimeType($providedMimeType, $extractedMimeType, null);
 219              return;
 220          }
 221          // Deprecated: local file path detection (kept for backward compatibility, before the base64 check).
 222          // Stream wrappers were rejected above, so this cannot open a network connection.
 223          if (strpos($file, "\x00") === \false && is_file($file)) {
 224              @trigger_error('Passing a local file path to ' . self::class . '::__construct() is deprecated since 1.5.1; ' . 'use File::fromLocalPath() instead.', \E_USER_DEPRECATED);
 225              $local = self::fromLocalPath($file, $providedMimeType);
 226              $this->fileType = $local->fileType;
 227              $this->base64Data = $local->base64Data;
 228              $this->mimeType = $local->mimeType;
 229              return;
 230          }
 231          // Check if it's plain base64.
 232          if (self::isBase64($file)) {
 233              if ($providedMimeType === null) {
 234                  throw new InvalidArgumentException('MIME type is required when providing plain base64 data without data URI format.');
 235              }
 236              $this->fileType = FileTypeEnum::inline();
 237              $this->base64Data = $file;
 238              $this->mimeType = new MimeType($providedMimeType);
 239              return;
 240          }
 241          throw new InvalidArgumentException('Invalid file provided. Expected an http(s) URL, a data URI, base64 data with a MIME type, ' . 'or a valid local file path.');
 242      }
 243      /**
 244       * Checks if a string is an http(s) URL.
 245       *
 246       * @since 0.1.0
 247       * @since 1.5.1 Made static.
 248       *
 249       * @param string $string The string to check.
 250       * @return bool True if the string is an http(s) URL.
 251       */
 252      private static function isHttpUrl(string $string): bool
 253      {
 254          return filter_var($string, \FILTER_VALIDATE_URL) !== \false && preg_match('/^https?:\/\//i', $string) === 1;
 255      }
 256      /**
 257       * Checks if a string is plain base64 data (possibly empty).
 258       *
 259       * @since 1.5.1
 260       *
 261       * @param string $string The string to check.
 262       * @return bool True if the string is plain base64 data.
 263       */
 264      private static function isBase64(string $string): bool
 265      {
 266          return preg_match(self::BASE64_PATTERN, $string) === 1;
 267      }
 268      /**
 269       * Gets the file type.
 270       *
 271       * @since 0.1.0
 272       *
 273       * @return FileTypeEnum The file type.
 274       */
 275      public function getFileType(): FileTypeEnum
 276      {
 277          return $this->fileType;
 278      }
 279      /**
 280       * Checks if the file is an inline file.
 281       *
 282       * @since 0.1.0
 283       *
 284       * @return bool True if the file is inline (base64/data URI).
 285       */
 286      public function isInline(): bool
 287      {
 288          return $this->fileType->isInline();
 289      }
 290      /**
 291       * Checks if the file is a remote file.
 292       *
 293       * @since 0.1.0
 294       *
 295       * @return bool True if the file is remote (URL).
 296       */
 297      public function isRemote(): bool
 298      {
 299          return $this->fileType->isRemote();
 300      }
 301      /**
 302       * Gets the URL for remote files.
 303       *
 304       * @since 0.1.0
 305       *
 306       * @return string|null The URL, or null if not a remote file.
 307       */
 308      public function getUrl(): ?string
 309      {
 310          return $this->url;
 311      }
 312      /**
 313       * Gets the base64-encoded data for inline files.
 314       *
 315       * @since 0.1.0
 316       *
 317       * @return string|null The plain base64-encoded data (without data URI prefix), or null if not an inline file.
 318       */
 319      public function getBase64Data(): ?string
 320      {
 321          return $this->base64Data;
 322      }
 323      /**
 324       * Gets the data as a data URI for inline files.
 325       *
 326       * @since 0.1.0
 327       *
 328       * @return string|null The data URI in format: data:[mimeType];base64,[data], or null if not an inline file.
 329       */
 330      public function getDataUri(): ?string
 331      {
 332          if ($this->base64Data === null) {
 333              return null;
 334          }
 335          return sprintf('data:%s;base64,%s', $this->getMimeType(), $this->base64Data);
 336      }
 337      /**
 338       * Gets the MIME type of the file as a string.
 339       *
 340       * @since 0.1.0
 341       *
 342       * @return string The MIME type string value.
 343       */
 344      public function getMimeType(): string
 345      {
 346          return (string) $this->mimeType;
 347      }
 348      /**
 349       * Gets the MIME type object.
 350       *
 351       * @since 0.1.0
 352       *
 353       * @return MimeType The MIME type object.
 354       */
 355      public function getMimeTypeObject(): MimeType
 356      {
 357          return $this->mimeType;
 358      }
 359      /**
 360       * Checks if the file is a video.
 361       *
 362       * @since 0.1.0
 363       *
 364       * @return bool True if the file is a video.
 365       */
 366      public function isVideo(): bool
 367      {
 368          return $this->mimeType->isVideo();
 369      }
 370      /**
 371       * Checks if the file is an image.
 372       *
 373       * @since 0.1.0
 374       *
 375       * @return bool True if the file is an image.
 376       */
 377      public function isImage(): bool
 378      {
 379          return $this->mimeType->isImage();
 380      }
 381      /**
 382       * Checks if the file is audio.
 383       *
 384       * @since 0.1.0
 385       *
 386       * @return bool True if the file is audio.
 387       */
 388      public function isAudio(): bool
 389      {
 390          return $this->mimeType->isAudio();
 391      }
 392      /**
 393       * Checks if the file is text.
 394       *
 395       * @since 0.1.0
 396       *
 397       * @return bool True if the file is text.
 398       */
 399      public function isText(): bool
 400      {
 401          return $this->mimeType->isText();
 402      }
 403      /**
 404       * Checks if the file is a document.
 405       *
 406       * @since 0.1.0
 407       *
 408       * @return bool True if the file is a document.
 409       */
 410      public function isDocument(): bool
 411      {
 412          return $this->mimeType->isDocument();
 413      }
 414      /**
 415       * Checks if the file is a specific MIME type.
 416       *
 417       * @since 0.1.0
 418       *
 419       * @param string $type The mime type to check (e.g. 'image', 'text', 'video', 'audio').
 420       *
 421       * @return bool True if the file is of the specified type.
 422       */
 423      public function isMimeType(string $type): bool
 424      {
 425          return $this->mimeType->isType($type);
 426      }
 427      /**
 428       * Determines the MIME type from various sources.
 429       *
 430       * @since 0.1.0
 431       * @since 1.5.1 Made static.
 432       *
 433       * @param string|null $providedMimeType The explicitly provided MIME type.
 434       * @param string|null $extractedMimeType The MIME type extracted from data URI.
 435       * @param string|null $pathOrUrl The file path or URL to extract extension from.
 436       * @return MimeType The determined MIME type.
 437       * @throws InvalidArgumentException If MIME type cannot be determined.
 438       */
 439      private static function determineMimeType(?string $providedMimeType, ?string $extractedMimeType, ?string $pathOrUrl): MimeType
 440      {
 441          // Prefer explicitly provided MIME type
 442          if ($providedMimeType !== null) {
 443              return new MimeType($providedMimeType);
 444          }
 445          // Use extracted MIME type from data URI
 446          if ($extractedMimeType !== null) {
 447              return new MimeType($extractedMimeType);
 448          }
 449          // Try to determine from file extension
 450          if ($pathOrUrl !== null) {
 451              $parsedUrl = parse_url($pathOrUrl);
 452              $path = $parsedUrl['path'] ?? $pathOrUrl;
 453              // Remove query string and fragment if present
 454              $cleanPath = strtok($path, '?#');
 455              if ($cleanPath === \false) {
 456                  $cleanPath = $path;
 457              }
 458              $extension = pathinfo($cleanPath, \PATHINFO_EXTENSION);
 459              if (!empty($extension)) {
 460                  try {
 461                      return MimeType::fromExtension($extension);
 462                  } catch (InvalidArgumentException $e) {
 463                      // Extension not recognized, continue to error
 464                      unset($e);
 465                  }
 466              }
 467          }
 468          throw new InvalidArgumentException('Unable to determine MIME type. Please provide it explicitly.');
 469      }
 470      /**
 471       * {@inheritDoc}
 472       *
 473       * @since 0.1.0
 474       */
 475      public static function getJsonSchema(): array
 476      {
 477          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]]]];
 478      }
 479      /**
 480       * {@inheritDoc}
 481       *
 482       * @since 0.1.0
 483       *
 484       * @return FileArrayShape
 485       */
 486      public function toArray(): array
 487      {
 488          $data = [self::KEY_FILE_TYPE => $this->fileType->value, self::KEY_MIME_TYPE => $this->getMimeType()];
 489          if ($this->url !== null) {
 490              $data[self::KEY_URL] = $this->url;
 491          } elseif (!$this->fileType->isRemote() && $this->base64Data !== null) {
 492              $data[self::KEY_BASE64_DATA] = $this->base64Data;
 493          } else {
 494              throw new RuntimeException('File requires either url or base64Data. This should not be a possible condition.');
 495          }
 496          return $data;
 497      }
 498      /**
 499       * {@inheritDoc}
 500       *
 501       * This check is purely structural and never accesses the filesystem.
 502       *
 503       * @since 1.5.1
 504       */
 505      public static function isArrayShape(array $array): bool
 506      {
 507          try {
 508              self::assertArrayShape($array);
 509              return \true;
 510          } catch (InvalidArgumentException $e) {
 511              return \false;
 512          }
 513      }
 514      /**
 515       * {@inheritDoc}
 516       *
 517       * @since 0.1.0
 518       * @since 1.5.1 The fileType key is authoritative: remote files require an http(s) url, inline files
 519       *                require base64Data and mimeType. Local file paths are rejected.
 520       */
 521      public static function fromArray(array $array): self
 522      {
 523          self::assertArrayShape($array);
 524          $mimeType = $array[self::KEY_MIME_TYPE] ?? null;
 525          if ($array[self::KEY_FILE_TYPE] === FileTypeEnum::REMOTE) {
 526              return self::fromUrl($array[self::KEY_URL] ?? '', $mimeType);
 527          }
 528          return self::fromBase64($array[self::KEY_BASE64_DATA] ?? '', $mimeType ?? '');
 529      }
 530      /**
 531       * Asserts that an array has a valid file array shape, without constructing the file.
 532       *
 533       * @since 1.5.1
 534       *
 535       * @param array<mixed> $array The array to check.
 536       * @throws InvalidArgumentException If the array is not a valid file array shape.
 537       */
 538      private static function assertArrayShape(array $array): void
 539      {
 540          static::validateFromArrayData($array, [self::KEY_FILE_TYPE]);
 541          $fileType = $array[self::KEY_FILE_TYPE];
 542          if ($fileType !== FileTypeEnum::REMOTE && $fileType !== FileTypeEnum::INLINE) {
 543              throw new InvalidArgumentException(sprintf('Invalid file type. Expected "%s" or "%s".', FileTypeEnum::REMOTE, FileTypeEnum::INLINE));
 544          }
 545          // A null mimeType is treated like an absent one (for remote files it is then inferred from the URL).
 546          $mimeType = $array[self::KEY_MIME_TYPE] ?? null;
 547          if ($mimeType !== null && (!is_string($mimeType) || !MimeType::isValid($mimeType))) {
 548              throw new InvalidArgumentException('Invalid MIME type provided in file array.');
 549          }
 550          if ($fileType === FileTypeEnum::REMOTE) {
 551              if (array_key_exists(self::KEY_BASE64_DATA, $array)) {
 552                  throw new InvalidArgumentException('Remote files must not contain base64Data.');
 553              }
 554              $url = $array[self::KEY_URL] ?? null;
 555              if (!is_string($url) || !self::isHttpUrl($url)) {
 556                  throw new InvalidArgumentException('Remote files require a url with an http:// or https:// scheme.');
 557              }
 558              if ($mimeType === null) {
 559                  // Without an explicit MIME type it must be inferable from the URL (no I/O involved).
 560                  self::determineMimeType(null, null, $url);
 561              }
 562              return;
 563          }
 564          if (array_key_exists(self::KEY_URL, $array)) {
 565              throw new InvalidArgumentException('Inline files must not contain url.');
 566          }
 567          $base64Data = $array[self::KEY_BASE64_DATA] ?? null;
 568          if (!is_string($base64Data)) {
 569              throw new InvalidArgumentException('Inline files require base64Data.');
 570          }
 571          if (str_starts_with($base64Data, 'data:')) {
 572              throw new InvalidArgumentException('Inline files require raw base64 data in base64Data, not a data URI.');
 573          }
 574          if (!self::isBase64($base64Data)) {
 575              throw new InvalidArgumentException('Inline files require valid base64 data in base64Data. Local file paths are not supported.');
 576          }
 577          if ($mimeType === null) {
 578              throw new InvalidArgumentException('Inline files require a mimeType.');
 579          }
 580      }
 581      /**
 582       * Performs a deep clone of the file.
 583       *
 584       * This method ensures that the MimeType value object is cloned to prevent
 585       * any shared references between the original and cloned file.
 586       *
 587       * @since 0.4.2
 588       */
 589      public function __clone()
 590      {
 591          $this->mimeType = clone $this->mimeType;
 592      }
 593  }


Generated : Sun Nov 1 08:20:35 2026 Cross-referenced by PHPXref