[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/SimplePie/src/ -> IRI.php (source)

   1  <?php
   2  
   3  // SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
   4  // SPDX-FileCopyrightText: 2008 Steve Minutillo
   5  // SPDX-License-Identifier: BSD-3-Clause
   6  
   7  declare(strict_types=1);
   8  
   9  namespace SimplePie;
  10  
  11  /**
  12   * IRI parser/serialiser/normaliser
  13   *
  14   * @property ?string $scheme
  15   * @property ?string $userinfo
  16   * @property ?string $host
  17   * @property ?int $port
  18   * @property-write int|string|null $port
  19   * @property ?string $authority
  20   * @property string $path
  21   * @property ?string $query
  22   * @property ?string $fragment
  23   */
  24  class IRI
  25  {
  26      /**
  27       * Scheme
  28       *
  29       * @var ?string
  30       */
  31      protected $scheme = null;
  32  
  33      /**
  34       * User Information
  35       *
  36       * @var ?string
  37       */
  38      protected $iuserinfo = null;
  39  
  40      /**
  41       * ihost
  42       *
  43       * @var ?string
  44       */
  45      protected $ihost = null;
  46  
  47      /**
  48       * Port
  49       *
  50       * @var ?int
  51       */
  52      protected $port = null;
  53  
  54      /**
  55       * ipath
  56       *
  57       * @var string
  58       */
  59      protected $ipath = '';
  60  
  61      /**
  62       * iquery
  63       *
  64       * @var ?string
  65       */
  66      protected $iquery = null;
  67  
  68      /**
  69       * ifragment
  70       *
  71       * @var ?string
  72       */
  73      protected $ifragment = null;
  74  
  75      /**
  76       * Normalization database
  77       *
  78       * Each key is the scheme, each value is an array with each key as the IRI
  79       * part and value as the default value for that part.
  80       *
  81       * @var array<string, array<string, mixed>>
  82       */
  83      protected $normalization = [
  84          'acap' => [
  85              'port' => 674
  86          ],
  87          'dict' => [
  88              'port' => 2628
  89          ],
  90          'file' => [
  91              'ihost' => 'localhost'
  92          ],
  93          'http' => [
  94              'port' => 80,
  95              'ipath' => '/'
  96          ],
  97          'https' => [
  98              'port' => 443,
  99              'ipath' => '/'
 100          ],
 101      ];
 102  
 103      /**
 104       * Return the entire IRI when you try and read the object as a string
 105       *
 106       * @return string
 107       */
 108      public function __toString()
 109      {
 110          return (string) $this->get_iri();
 111      }
 112  
 113      /**
 114       * Overload __set() to provide access via properties
 115       *
 116       * @param string $name Property name
 117       * @param mixed $value Property value
 118       * @return void
 119       */
 120      public function __set(string $name, $value)
 121      {
 122          $callable = [$this, 'set_' . $name];
 123          if (is_callable($callable)) {
 124              call_user_func($callable, $value);
 125          } elseif (
 126              $name === 'iauthority'
 127              || $name === 'iuserinfo'
 128              || $name === 'ihost'
 129              || $name === 'ipath'
 130              || $name === 'iquery'
 131              || $name === 'ifragment'
 132          ) {
 133              call_user_func([$this, 'set_' . substr($name, 1)], $value);
 134          }
 135      }
 136  
 137      /**
 138       * Overload __get() to provide access via properties
 139       *
 140       * @param string $name Property name
 141       * @return mixed
 142       */
 143      public function __get(string $name)
 144      {
 145          // isset() returns false for null, we don't want to do that
 146          // Also why we use array_key_exists below instead of isset()
 147          $props = get_object_vars($this);
 148  
 149          if (
 150              $name === 'iri' ||
 151              $name === 'uri' ||
 152              $name === 'iauthority' ||
 153              $name === 'authority'
 154          ) {
 155              $return = $this->{"get_$name"}();
 156          } elseif (array_key_exists($name, $props)) {
 157              $return = $this->$name;
 158          }
 159          // host -> ihost
 160          elseif (array_key_exists($prop = 'i' . $name, $props)) {
 161              $name = $prop;
 162              $return = $this->$prop;
 163          }
 164          // ischeme -> scheme
 165          elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props)) {
 166              $name = $prop;
 167              $return = $this->$prop;
 168          } else {
 169              trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE);
 170              $return = null;
 171          }
 172  
 173          if ($return === null && isset($this->scheme, $this->normalization[$this->scheme][$name])) {
 174              return $this->normalization[$this->scheme][$name];
 175          }
 176  
 177          return $return;
 178      }
 179  
 180      /**
 181       * Overload __isset() to provide access via properties
 182       *
 183       * @param string $name Property name
 184       * @return bool
 185       */
 186      public function __isset(string $name)
 187      {
 188          return method_exists($this, 'get_' . $name) || isset($this->$name);
 189      }
 190  
 191      /**
 192       * Overload __unset() to provide access via properties
 193       *
 194       * @param string $name Property name
 195       * @return void
 196       */
 197      public function __unset(string $name)
 198      {
 199          $callable = [$this, 'set_' . $name];
 200          if (is_callable($callable)) {
 201              call_user_func($callable, '');
 202          }
 203      }
 204  
 205      /**
 206       * Create a new IRI object, from a specified string
 207       *
 208       * @param string|null $iri
 209       */
 210      public function __construct(?string $iri = null)
 211      {
 212          $this->set_iri($iri);
 213      }
 214  
 215      /**
 216       * Clean up
 217       * @return void
 218       */
 219      public function __destruct()
 220      {
 221          $this->set_iri(null, true);
 222          $this->set_path(null, true);
 223          $this->set_authority(null, true);
 224      }
 225  
 226      /**
 227       * Create a new IRI object by resolving a relative IRI
 228       *
 229       * Returns false if $base is not absolute, otherwise an IRI.
 230       *
 231       * @param IRI|string $base (Absolute) Base IRI
 232       * @param IRI|string $relative Relative IRI
 233       * @return IRI|false
 234       */
 235      public static function absolutize($base, $relative)
 236      {
 237          if (!($relative instanceof IRI)) {
 238              $relative = new IRI($relative);
 239          }
 240          if (!$relative->is_valid()) {
 241              return false;
 242          } elseif ($relative->scheme !== null) {
 243              return clone $relative;
 244          } else {
 245              if (!($base instanceof IRI)) {
 246                  $base = new IRI($base);
 247              }
 248              if ($base->scheme !== null && $base->is_valid()) {
 249                  if ($relative->get_iri() !== '') {
 250                      if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null) {
 251                          $target = clone $relative;
 252                          $target->scheme = $base->scheme;
 253                      } else {
 254                          $target = new IRI();
 255                          $target->scheme = $base->scheme;
 256                          $target->iuserinfo = $base->iuserinfo;
 257                          $target->ihost = $base->ihost;
 258                          $target->port = $base->port;
 259                          if ($relative->ipath !== '') {
 260                              if ($relative->ipath[0] === '/') {
 261                                  $target->ipath = $relative->ipath;
 262                              } elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '') {
 263                                  $target->ipath = '/' . $relative->ipath;
 264                              } elseif (($last_segment = strrpos($base->ipath, '/')) !== false) {
 265                                  $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath;
 266                              } else {
 267                                  $target->ipath = $relative->ipath;
 268                              }
 269                              $target->ipath = $target->remove_dot_segments($target->ipath);
 270                              $target->iquery = $relative->iquery;
 271                          } else {
 272                              $target->ipath = $base->ipath;
 273                              if ($relative->iquery !== null) {
 274                                  $target->iquery = $relative->iquery;
 275                              } elseif ($base->iquery !== null) {
 276                                  $target->iquery = $base->iquery;
 277                              }
 278                          }
 279                          $target->ifragment = $relative->ifragment;
 280                      }
 281                  } else {
 282                      $target = clone $base;
 283                      $target->ifragment = null;
 284                  }
 285                  $target->scheme_normalization();
 286                  return $target;
 287              }
 288  
 289              return false;
 290          }
 291      }
 292  
 293      /**
 294       * Parse an IRI into scheme/authority/path/query/fragment segments
 295       *
 296       * @param string $iri
 297       * @return array{
 298       *   scheme: string|null,
 299       *   authority: string|null,
 300       *   path: string,
 301       *   query: string|null,
 302       *   fragment: string|null,
 303       * }|false
 304       */
 305      protected function parse_iri(string $iri)
 306      {
 307          $iri = trim($iri, "\x20\x09\x0A\x0C\x0D");
 308          if (preg_match('/^(?:(?P<scheme>[^:\/?#]+):)?(:?\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(?:\?(?P<query>[^#]*))?(?:#(?P<fragment>.*))?$/', $iri, $match, \PREG_UNMATCHED_AS_NULL)) {
 309              // TODO: Remove once we require PHP ≥ 7.4.
 310              $match['query'] = $match['query'] ?? null;
 311              $match['fragment'] = $match['fragment'] ?? null;
 312              return $match;
 313          }
 314  
 315          // This can occur when a paragraph is accidentally parsed as a URI
 316          return false;
 317      }
 318  
 319      /**
 320       * Remove dot segments from a path
 321       *
 322       * @param string $input
 323       * @return string
 324       */
 325      protected function remove_dot_segments(string $input)
 326      {
 327          $output = '';
 328          while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') {
 329              // A: If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise,
 330              if (strpos($input, '../') === 0) {
 331                  $input = substr($input, 3);
 332              } elseif (strpos($input, './') === 0) {
 333                  $input = substr($input, 2);
 334              }
 335              // 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,
 336              elseif (strpos($input, '/./') === 0) {
 337                  $input = substr($input, 2);
 338              } elseif ($input === '/.') {
 339                  $input = '/';
 340              }
 341              // 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,
 342              elseif (strpos($input, '/../') === 0) {
 343                  $input = substr($input, 3);
 344                  $output = substr_replace($output, '', intval(strrpos($output, '/')));
 345              } elseif ($input === '/..') {
 346                  $input = '/';
 347                  $output = substr_replace($output, '', intval(strrpos($output, '/')));
 348              }
 349              // D: if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise,
 350              elseif ($input === '.' || $input === '..') {
 351                  $input = '';
 352              }
 353              // 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
 354              elseif (($pos = strpos($input, '/', 1)) !== false) {
 355                  $output .= substr($input, 0, $pos);
 356                  $input = substr_replace($input, '', 0, $pos);
 357              } else {
 358                  $output .= $input;
 359                  $input = '';
 360              }
 361          }
 362          return $output . $input;
 363      }
 364  
 365      /**
 366       * Replace invalid character with percent encoding
 367       *
 368       * @param string $string Input string
 369       * @param string $extra_chars Valid characters not in iunreserved or
 370       *                            iprivate (this is ASCII-only)
 371       * @param bool $iprivate Allow iprivate
 372       * @return string
 373       */
 374      protected function replace_invalid_with_pct_encoding(string $string, string $extra_chars, bool $iprivate = false)
 375      {
 376          // Normalize as many pct-encoded sections as possible
 377          $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', [$this, 'remove_iunreserved_percent_encoded'], $string);
 378          \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid");
 379  
 380          // Replace invalid percent characters
 381          $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string);
 382          \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid");
 383  
 384          // Add unreserved and % to $extra_chars (the latter is safe because all
 385          // pct-encoded sections are now valid).
 386          $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%';
 387  
 388          // Now replace any bytes that aren't allowed with their pct-encoded versions
 389          $position = 0;
 390          $strlen = strlen($string);
 391          while (($position += strspn($string, $extra_chars, $position)) < $strlen) {
 392              $value = ord($string[$position]);
 393              $character = 0;
 394  
 395              // Start position
 396              $start = $position;
 397  
 398              // By default we are valid
 399              $valid = true;
 400  
 401              // No one byte sequences are valid due to the while.
 402              // Two byte sequence:
 403              if (($value & 0xE0) === 0xC0) {
 404                  $character = ($value & 0x1F) << 6;
 405                  $length = 2;
 406                  $remaining = 1;
 407              }
 408              // Three byte sequence:
 409              elseif (($value & 0xF0) === 0xE0) {
 410                  $character = ($value & 0x0F) << 12;
 411                  $length = 3;
 412                  $remaining = 2;
 413              }
 414              // Four byte sequence:
 415              elseif (($value & 0xF8) === 0xF0) {
 416                  $character = ($value & 0x07) << 18;
 417                  $length = 4;
 418                  $remaining = 3;
 419              }
 420              // Invalid byte:
 421              else {
 422                  $valid = false;
 423                  $length = 1;
 424                  $remaining = 0;
 425              }
 426  
 427              if ($remaining) {
 428                  if ($position + $length <= $strlen) {
 429                      for ($position++; $remaining; $position++) {
 430                          $value = ord($string[$position]);
 431  
 432                          // Check that the byte is valid, then add it to the character:
 433                          if (($value & 0xC0) === 0x80) {
 434                              $character |= ($value & 0x3F) << (--$remaining * 6);
 435                          }
 436                          // If it is invalid, count the sequence as invalid and reprocess the current byte:
 437                          else {
 438                              $valid = false;
 439                              $position--;
 440                              break;
 441                          }
 442                      }
 443                  } else {
 444                      $position = $strlen - 1;
 445                      $valid = false;
 446                  }
 447              }
 448  
 449              // Percent encode anything invalid or not in ucschar
 450              if (
 451                  // Invalid sequences
 452                  !$valid
 453                  // Non-shortest form sequences are invalid
 454                  || $length > 1 && $character <= 0x7F
 455                  || $length > 2 && $character <= 0x7FF
 456                  || $length > 3 && $character <= 0xFFFF
 457                  // Outside of range of ucschar codepoints
 458                  // Noncharacters
 459                  || ($character & 0xFFFE) === 0xFFFE
 460                  || $character >= 0xFDD0 && $character <= 0xFDEF
 461                  || (
 462                      // Everything else not in ucschar
 463                      $character > 0xD7FF && $character < 0xF900
 464                      || $character < 0xA0
 465                      || $character > 0xEFFFD
 466                  )
 467                  && (
 468                      // Everything not in iprivate, if it applies
 469                      !$iprivate
 470                      || $character < 0xE000
 471                      || $character > 0x10FFFD
 472                  )
 473              ) {
 474                  // If we were a character, pretend we weren't, but rather an error.
 475                  if ($valid) {
 476                      $position--;
 477                  }
 478  
 479                  for ($j = $start; $j <= $position; $j++) {
 480                      $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1);
 481                      $j += 2;
 482                      $position += 2;
 483                      $strlen += 2;
 484                  }
 485              }
 486          }
 487  
 488          return $string;
 489      }
 490  
 491      /**
 492       * Callback function for preg_replace_callback.
 493       *
 494       * Removes sequences of percent encoded bytes that represent UTF-8
 495       * encoded characters in iunreserved
 496       *
 497       * @param array{string} $match PCRE match, a capture group #0 consisting of a sequence of valid percent-encoded bytes
 498       * @return string Replacement
 499       */
 500      protected function remove_iunreserved_percent_encoded(array $match)
 501      {
 502          // As we just have valid percent encoded sequences we can just explode
 503          // and ignore the first member of the returned array (an empty string).
 504          $bytes = explode('%', $match[0]);
 505  
 506          // Initialize the new string (this is what will be returned) and that
 507          // there are no bytes remaining in the current sequence (unsurprising
 508          // at the first byte!).
 509          $string = '';
 510          $remaining = 0;
 511  
 512          // these variables will be initialized in the loop but PHPStan is not able to detect it currently
 513          $start = 0;
 514          $character = 0;
 515          $length = 0;
 516          $valid = true;
 517  
 518          // Loop over each and every byte, and set $value to its value
 519          for ($i = 1, $len = count($bytes); $i < $len; $i++) {
 520              $value = hexdec($bytes[$i]);
 521  
 522              // If we're the first byte of sequence:
 523              if (!$remaining) {
 524                  // Start position
 525                  $start = $i;
 526  
 527                  // By default we are valid
 528                  $valid = true;
 529  
 530                  // One byte sequence:
 531                  if ($value <= 0x7F) {
 532                      $character = $value;
 533                      $length = 1;
 534                  }
 535                  // Two byte sequence:
 536                  elseif (($value & 0xE0) === 0xC0) {
 537                      $character = ($value & 0x1F) << 6;
 538                      $length = 2;
 539                      $remaining = 1;
 540                  }
 541                  // Three byte sequence:
 542                  elseif (($value & 0xF0) === 0xE0) {
 543                      $character = ($value & 0x0F) << 12;
 544                      $length = 3;
 545                      $remaining = 2;
 546                  }
 547                  // Four byte sequence:
 548                  elseif (($value & 0xF8) === 0xF0) {
 549                      $character = ($value & 0x07) << 18;
 550                      $length = 4;
 551                      $remaining = 3;
 552                  }
 553                  // Invalid byte:
 554                  else {
 555                      $valid = false;
 556                      $remaining = 0;
 557                  }
 558              }
 559              // Continuation byte:
 560              else {
 561                  // Check that the byte is valid, then add it to the character:
 562                  if (($value & 0xC0) === 0x80) {
 563                      $remaining--;
 564                      $character |= ($value & 0x3F) << ($remaining * 6);
 565                  }
 566                  // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence:
 567                  else {
 568                      $valid = false;
 569                      $remaining = 0;
 570                      $i--;
 571                  }
 572              }
 573  
 574              // If we've reached the end of the current byte sequence, append it to Unicode::$data
 575              if (!$remaining) {
 576                  // Percent encode anything invalid or not in iunreserved
 577                  if (
 578                      // Invalid sequences
 579                      !$valid
 580                      // Non-shortest form sequences are invalid
 581                      || $length > 1 && $character <= 0x7F
 582                      || $length > 2 && $character <= 0x7FF
 583                      || $length > 3 && $character <= 0xFFFF
 584                      // Outside of range of iunreserved codepoints
 585                      || $character < 0x2D
 586                      || $character > 0xEFFFD
 587                      // Noncharacters
 588                      || ($character & 0xFFFE) === 0xFFFE
 589                      || $character >= 0xFDD0 && $character <= 0xFDEF
 590                      // Everything else not in iunreserved (this is all BMP)
 591                      || $character === 0x2F
 592                      || $character > 0x39 && $character < 0x41
 593                      || $character > 0x5A && $character < 0x61
 594                      || $character > 0x7A && $character < 0x7E
 595                      || $character > 0x7E && $character < 0xA0
 596                      || $character > 0xD7FF && $character < 0xF900
 597                  ) {
 598                      for ($j = $start; $j <= $i; $j++) {
 599                          $string .= '%' . strtoupper($bytes[$j]);
 600                      }
 601                  } else {
 602                      for ($j = $start; $j <= $i; $j++) {
 603                          // Cast for PHPStan, this will always be a number between 0 and 0xFF so hexdec will return int.
 604                          $string .= chr((int) hexdec($bytes[$j]));
 605                      }
 606                  }
 607              }
 608          }
 609  
 610          // If we have any bytes left over they are invalid (i.e., we are
 611          // mid-way through a multi-byte sequence)
 612          if ($remaining) {
 613              for ($j = $start; $j < $len; $j++) {
 614                  $string .= '%' . strtoupper($bytes[$j]);
 615              }
 616          }
 617  
 618          return $string;
 619      }
 620  
 621      /**
 622       * @return void
 623       */
 624      protected function scheme_normalization()
 625      {
 626          if ($this->scheme === null) {
 627              return;
 628          }
 629  
 630          if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) {
 631              $this->iuserinfo = null;
 632          }
 633          if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) {
 634              $this->ihost = null;
 635          }
 636          if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) {
 637              $this->port = null;
 638          }
 639          if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) {
 640              $this->ipath = '';
 641          }
 642          if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) {
 643              $this->iquery = null;
 644          }
 645          if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) {
 646              $this->ifragment = null;
 647          }
 648      }
 649  
 650      /**
 651       * Check if the object represents a valid IRI. This needs to be done on each
 652       * call as some things change depending on another part of the IRI.
 653       *
 654       * @return bool
 655       */
 656      public function is_valid()
 657      {
 658          if ($this->ipath === '') {
 659              return true;
 660          }
 661  
 662          $isauthority = $this->iuserinfo !== null || $this->ihost !== null ||
 663              $this->port !== null;
 664          if ($isauthority && $this->ipath[0] === '/') {
 665              return true;
 666          }
 667  
 668          if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) {
 669              return false;
 670          }
 671  
 672          // Relative urls cannot have a colon in the first path segment (and the
 673          // slashes themselves are not included so skip the first character).
 674          if (!$this->scheme && !$isauthority &&
 675              strpos($this->ipath, ':') !== false &&
 676              strpos($this->ipath, '/', 1) !== false &&
 677              strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) {
 678              return false;
 679          }
 680  
 681          return true;
 682      }
 683  
 684      /**
 685       * Set the entire IRI. Returns true on success, false on failure (if there
 686       * are any invalid characters).
 687       *
 688       * @param string|null $iri
 689       * @return bool
 690       */
 691      public function set_iri(?string $iri, bool $clear_cache = false)
 692      {
 693          static $cache;
 694          if ($clear_cache) {
 695              $cache = null;
 696              return false;
 697          }
 698          if (!$cache) {
 699              $cache = [];
 700          }
 701  
 702          if ($iri === null) {
 703              return true;
 704          } elseif (isset($cache[$iri])) {
 705              [
 706                  $this->scheme,
 707                  $this->iuserinfo,
 708                  $this->ihost,
 709                  $this->port,
 710                  $this->ipath,
 711                  $this->iquery,
 712                  $this->ifragment,
 713                  $return
 714              ] = $cache[$iri];
 715  
 716              return $return;
 717          }
 718  
 719          $parsed = $this->parse_iri((string) $iri);
 720          if (!$parsed) {
 721              return false;
 722          }
 723  
 724          $return = $this->set_scheme($parsed['scheme'])
 725              && $this->set_authority($parsed['authority'])
 726              && $this->set_path($parsed['path'])
 727              && $this->set_query($parsed['query'])
 728              && $this->set_fragment($parsed['fragment']);
 729  
 730          $cache[$iri] = [
 731              $this->scheme,
 732              $this->iuserinfo,
 733              $this->ihost,
 734              $this->port,
 735              $this->ipath,
 736              $this->iquery,
 737              $this->ifragment,
 738              $return
 739          ];
 740  
 741          return $return;
 742      }
 743  
 744      /**
 745       * Set the scheme. Returns true on success, false on failure (if there are
 746       * any invalid characters).
 747       *
 748       * @param string|null $scheme
 749       * @return bool
 750       */
 751      public function set_scheme(?string $scheme)
 752      {
 753          if ($scheme === null) {
 754              $this->scheme = null;
 755          } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) {
 756              $this->scheme = null;
 757              return false;
 758          } else {
 759              $this->scheme = strtolower($scheme);
 760          }
 761          return true;
 762      }
 763  
 764      /**
 765       * Set the authority. Returns true on success, false on failure (if there are
 766       * any invalid characters).
 767       *
 768       * @param string|null $authority
 769       * @return bool
 770       */
 771      public function set_authority(?string $authority, bool $clear_cache = false)
 772      {
 773          static $cache;
 774          if ($clear_cache) {
 775              $cache = null;
 776              return false;
 777          }
 778          if (!$cache) {
 779              $cache = [];
 780          }
 781  
 782          if ($authority === null) {
 783              $this->iuserinfo = null;
 784              $this->ihost = null;
 785              $this->port = null;
 786              return true;
 787          } elseif (isset($cache[$authority])) {
 788              [
 789                  $this->iuserinfo,
 790                  $this->ihost,
 791                  $this->port,
 792                  $return
 793              ] = $cache[$authority];
 794  
 795              return $return;
 796          }
 797  
 798          $remaining = $authority;
 799          if (($iuserinfo_end = strrpos($remaining, '@')) !== false) {
 800              // Cast for PHPStan on PHP < 8.0. It does not detect that
 801              // the range is not flipped so substr cannot return false.
 802              $iuserinfo = (string) substr($remaining, 0, $iuserinfo_end);
 803              $remaining = substr($remaining, $iuserinfo_end + 1);
 804          } else {
 805              $iuserinfo = null;
 806          }
 807          if (($port_start = strpos($remaining, ':', intval(strpos($remaining, ']')))) !== false) {
 808              $port = substr($remaining, $port_start + 1);
 809              if ($port === false) {
 810                  $port = null;
 811              }
 812              $remaining = substr($remaining, 0, $port_start);
 813          } else {
 814              $port = null;
 815          }
 816  
 817          $return = $this->set_userinfo($iuserinfo) &&
 818                    $this->set_host($remaining) &&
 819                    $this->set_port($port);
 820  
 821          $cache[$authority] = [
 822              $this->iuserinfo,
 823              $this->ihost,
 824              $this->port,
 825              $return
 826          ];
 827  
 828          return $return;
 829      }
 830  
 831      /**
 832       * Set the iuserinfo.
 833       *
 834       * @param string|null $iuserinfo
 835       * @return bool
 836       */
 837      public function set_userinfo(?string $iuserinfo)
 838      {
 839          if ($iuserinfo === null) {
 840              $this->iuserinfo = null;
 841          } else {
 842              $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
 843              $this->scheme_normalization();
 844          }
 845  
 846          return true;
 847      }
 848  
 849      /**
 850       * Set the ihost. Returns true on success, false on failure (if there are
 851       * any invalid characters).
 852       *
 853       * @param string|null $ihost
 854       * @return bool
 855       */
 856      public function set_host(?string $ihost)
 857      {
 858          if ($ihost === null) {
 859              $this->ihost = null;
 860              return true;
 861          } elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') {
 862              if (\SimplePie\Net\IPv6::check_ipv6(substr($ihost, 1, -1))) {
 863                  $this->ihost = '[' . \SimplePie\Net\IPv6::compress(substr($ihost, 1, -1)) . ']';
 864              } else {
 865                  $this->ihost = null;
 866                  return false;
 867              }
 868          } else {
 869              $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');
 870  
 871              // Lowercase, but ignore pct-encoded sections (as they should
 872              // remain uppercase). This must be done after the previous step
 873              // as that can add unescaped characters.
 874              $position = 0;
 875              $strlen = strlen($ihost);
 876              while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) {
 877                  if ($ihost[$position] === '%') {
 878                      $position += 3;
 879                  } else {
 880                      $ihost[$position] = strtolower($ihost[$position]);
 881                      $position++;
 882                  }
 883              }
 884  
 885              $this->ihost = $ihost;
 886          }
 887  
 888          $this->scheme_normalization();
 889  
 890          return true;
 891      }
 892  
 893      /**
 894       * Set the port. Returns true on success, false on failure (if there are
 895       * any invalid characters).
 896       *
 897       * @param string|int|null $port
 898       * @return bool
 899       */
 900      public function set_port($port)
 901      {
 902          if ($port === null) {
 903              $this->port = null;
 904              return true;
 905          } elseif (strspn((string) $port, '0123456789') === strlen((string) $port)) {
 906              $this->port = (int) $port;
 907              $this->scheme_normalization();
 908              return true;
 909          }
 910  
 911          $this->port = null;
 912          return false;
 913      }
 914  
 915      /**
 916       * Set the ipath.
 917       *
 918       * @param string|null $ipath
 919       * @return bool
 920       */
 921      public function set_path(?string $ipath, bool $clear_cache = false)
 922      {
 923          static $cache;
 924          if ($clear_cache) {
 925              $cache = null;
 926              return false;
 927          }
 928          if (!$cache) {
 929              $cache = [];
 930          }
 931  
 932          $ipath = (string) $ipath;
 933  
 934          if (isset($cache[$ipath])) {
 935              $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
 936          } else {
 937              $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
 938              $removed = $this->remove_dot_segments($valid);
 939  
 940              $cache[$ipath] = [$valid, $removed];
 941              $this->ipath =  ($this->scheme !== null) ? $removed : $valid;
 942          }
 943  
 944          $this->scheme_normalization();
 945          return true;
 946      }
 947  
 948      /**
 949       * Set the iquery.
 950       *
 951       * @param string|null $iquery
 952       * @return bool
 953       */
 954      public function set_query(?string $iquery)
 955      {
 956          if ($iquery === null) {
 957              $this->iquery = null;
 958          } else {
 959              $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
 960              $this->scheme_normalization();
 961          }
 962          return true;
 963      }
 964  
 965      /**
 966       * Set the ifragment.
 967       *
 968       * @param string|null $ifragment
 969       * @return bool
 970       */
 971      public function set_fragment(?string $ifragment)
 972      {
 973          if ($ifragment === null) {
 974              $this->ifragment = null;
 975          } else {
 976              $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
 977              $this->scheme_normalization();
 978          }
 979          return true;
 980      }
 981  
 982      /**
 983       * Convert an IRI to a URI (or parts thereof)
 984       *
 985       * @param string $string
 986       * @return string
 987       */
 988      public function to_uri(string $string)
 989      {
 990          static $non_ascii;
 991          if (!$non_ascii) {
 992              $non_ascii = implode('', range("\x80", "\xFF"));
 993          }
 994  
 995          $position = 0;
 996          $strlen = strlen($string);
 997          while (($position += strcspn($string, $non_ascii, $position)) < $strlen) {
 998              $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1);
 999              $position += 3;
1000              $strlen += 2;
1001          }
1002  
1003          return $string;
1004      }
1005  
1006      /**
1007       * Get the complete IRI
1008       *
1009       * @return string|false
1010       */
1011      public function get_iri()
1012      {
1013          if (!$this->is_valid()) {
1014              return false;
1015          }
1016  
1017          $iri = '';
1018          if ($this->scheme !== null) {
1019              $iri .= $this->scheme . ':';
1020          }
1021          if (($iauthority = $this->get_iauthority()) !== null) {
1022              $iri .= '//' . $iauthority;
1023          }
1024          if ($this->ipath !== '') {
1025              $iri .= $this->ipath;
1026          } elseif (!empty($this->normalization[$this->scheme]['ipath']) && $iauthority !== null && $iauthority !== '') {
1027              $iri .= $this->normalization[$this->scheme]['ipath'];
1028          }
1029          if ($this->iquery !== null) {
1030              $iri .= '?' . $this->iquery;
1031          }
1032          if ($this->ifragment !== null) {
1033              $iri .= '#' . $this->ifragment;
1034          }
1035  
1036          return $iri;
1037      }
1038  
1039      /**
1040       * Get the complete URI
1041       *
1042       * @return string
1043       */
1044      public function get_uri()
1045      {
1046          return $this->to_uri((string) $this->get_iri());
1047      }
1048  
1049      /**
1050       * Get the complete iauthority
1051       *
1052       * @return ?string
1053       */
1054      protected function get_iauthority()
1055      {
1056          if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null) {
1057              $iauthority = '';
1058              if ($this->iuserinfo !== null) {
1059                  $iauthority .= $this->iuserinfo . '@';
1060              }
1061              if ($this->ihost !== null) {
1062                  $iauthority .= $this->ihost;
1063              }
1064              if ($this->port !== null && $this->port !== 0) {
1065                  $iauthority .= ':' . $this->port;
1066              }
1067              return $iauthority;
1068          }
1069  
1070          return null;
1071      }
1072  
1073      /**
1074       * Get the complete authority
1075       *
1076       * @return ?string
1077       */
1078      protected function get_authority()
1079      {
1080          $iauthority = $this->get_iauthority();
1081          if (is_string($iauthority)) {
1082              return $this->to_uri($iauthority);
1083          }
1084  
1085          return $iauthority;
1086      }
1087  }
1088  
1089  class_alias('SimplePie\IRI', 'SimplePie_IRI');


Generated : Thu Oct 30 08:20:06 2025 Cross-referenced by PHPXref