[ 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->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 (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) {
 627              $this->iuserinfo = null;
 628          }
 629          if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost']) {
 630              $this->ihost = null;
 631          }
 632          if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port']) {
 633              $this->port = null;
 634          }
 635          if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath']) {
 636              $this->ipath = '';
 637          }
 638          if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery']) {
 639              $this->iquery = null;
 640          }
 641          if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment']) {
 642              $this->ifragment = null;
 643          }
 644      }
 645  
 646      /**
 647       * Check if the object represents a valid IRI. This needs to be done on each
 648       * call as some things change depending on another part of the IRI.
 649       *
 650       * @return bool
 651       */
 652      public function is_valid()
 653      {
 654          if ($this->ipath === '') {
 655              return true;
 656          }
 657  
 658          $isauthority = $this->iuserinfo !== null || $this->ihost !== null ||
 659              $this->port !== null;
 660          if ($isauthority && $this->ipath[0] === '/') {
 661              return true;
 662          }
 663  
 664          if (!$isauthority && (substr($this->ipath, 0, 2) === '//')) {
 665              return false;
 666          }
 667  
 668          // Relative urls cannot have a colon in the first path segment (and the
 669          // slashes themselves are not included so skip the first character).
 670          if (!$this->scheme && !$isauthority &&
 671              strpos($this->ipath, ':') !== false &&
 672              strpos($this->ipath, '/', 1) !== false &&
 673              strpos($this->ipath, ':') < strpos($this->ipath, '/', 1)) {
 674              return false;
 675          }
 676  
 677          return true;
 678      }
 679  
 680      /**
 681       * Set the entire IRI. Returns true on success, false on failure (if there
 682       * are any invalid characters).
 683       *
 684       * @param string|null $iri
 685       * @return bool
 686       */
 687      public function set_iri(?string $iri, bool $clear_cache = false)
 688      {
 689          static $cache;
 690          if ($clear_cache) {
 691              $cache = null;
 692              return false;
 693          }
 694          if (!$cache) {
 695              $cache = [];
 696          }
 697  
 698          if ($iri === null) {
 699              return true;
 700          } elseif (isset($cache[$iri])) {
 701              [
 702                  $this->scheme,
 703                  $this->iuserinfo,
 704                  $this->ihost,
 705                  $this->port,
 706                  $this->ipath,
 707                  $this->iquery,
 708                  $this->ifragment,
 709                  $return
 710              ] = $cache[$iri];
 711  
 712              return $return;
 713          }
 714  
 715          $parsed = $this->parse_iri((string) $iri);
 716          if (!$parsed) {
 717              return false;
 718          }
 719  
 720          $return = $this->set_scheme($parsed['scheme'])
 721              && $this->set_authority($parsed['authority'])
 722              && $this->set_path($parsed['path'])
 723              && $this->set_query($parsed['query'])
 724              && $this->set_fragment($parsed['fragment']);
 725  
 726          $cache[$iri] = [
 727              $this->scheme,
 728              $this->iuserinfo,
 729              $this->ihost,
 730              $this->port,
 731              $this->ipath,
 732              $this->iquery,
 733              $this->ifragment,
 734              $return
 735          ];
 736  
 737          return $return;
 738      }
 739  
 740      /**
 741       * Set the scheme. Returns true on success, false on failure (if there are
 742       * any invalid characters).
 743       *
 744       * @param string|null $scheme
 745       * @return bool
 746       */
 747      public function set_scheme(?string $scheme)
 748      {
 749          if ($scheme === null) {
 750              $this->scheme = null;
 751          } elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme)) {
 752              $this->scheme = null;
 753              return false;
 754          } else {
 755              $this->scheme = strtolower($scheme);
 756          }
 757          return true;
 758      }
 759  
 760      /**
 761       * Set the authority. Returns true on success, false on failure (if there are
 762       * any invalid characters).
 763       *
 764       * @param string|null $authority
 765       * @return bool
 766       */
 767      public function set_authority(?string $authority, bool $clear_cache = false)
 768      {
 769          static $cache;
 770          if ($clear_cache) {
 771              $cache = null;
 772              return false;
 773          }
 774          if (!$cache) {
 775              $cache = [];
 776          }
 777  
 778          if ($authority === null) {
 779              $this->iuserinfo = null;
 780              $this->ihost = null;
 781              $this->port = null;
 782              return true;
 783          } elseif (isset($cache[$authority])) {
 784              [
 785                  $this->iuserinfo,
 786                  $this->ihost,
 787                  $this->port,
 788                  $return
 789              ] = $cache[$authority];
 790  
 791              return $return;
 792          }
 793  
 794          $remaining = $authority;
 795          if (($iuserinfo_end = strrpos($remaining, '@')) !== false) {
 796              // Cast for PHPStan on PHP < 8.0. It does not detect that
 797              // the range is not flipped so substr cannot return false.
 798              $iuserinfo = (string) substr($remaining, 0, $iuserinfo_end);
 799              $remaining = substr($remaining, $iuserinfo_end + 1);
 800          } else {
 801              $iuserinfo = null;
 802          }
 803          if (($port_start = strpos($remaining, ':', intval(strpos($remaining, ']')))) !== false) {
 804              $port = substr($remaining, $port_start + 1);
 805              if ($port === false) {
 806                  $port = null;
 807              }
 808              $remaining = substr($remaining, 0, $port_start);
 809          } else {
 810              $port = null;
 811          }
 812  
 813          $return = $this->set_userinfo($iuserinfo) &&
 814                    $this->set_host($remaining) &&
 815                    $this->set_port($port);
 816  
 817          $cache[$authority] = [
 818              $this->iuserinfo,
 819              $this->ihost,
 820              $this->port,
 821              $return
 822          ];
 823  
 824          return $return;
 825      }
 826  
 827      /**
 828       * Set the iuserinfo.
 829       *
 830       * @param string|null $iuserinfo
 831       * @return bool
 832       */
 833      public function set_userinfo(?string $iuserinfo)
 834      {
 835          if ($iuserinfo === null) {
 836              $this->iuserinfo = null;
 837          } else {
 838              $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
 839              $this->scheme_normalization();
 840          }
 841  
 842          return true;
 843      }
 844  
 845      /**
 846       * Set the ihost. Returns true on success, false on failure (if there are
 847       * any invalid characters).
 848       *
 849       * @param string|null $ihost
 850       * @return bool
 851       */
 852      public function set_host(?string $ihost)
 853      {
 854          if ($ihost === null) {
 855              $this->ihost = null;
 856              return true;
 857          } elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']') {
 858              if (\SimplePie\Net\IPv6::check_ipv6(substr($ihost, 1, -1))) {
 859                  $this->ihost = '[' . \SimplePie\Net\IPv6::compress(substr($ihost, 1, -1)) . ']';
 860              } else {
 861                  $this->ihost = null;
 862                  return false;
 863              }
 864          } else {
 865              $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');
 866  
 867              // Lowercase, but ignore pct-encoded sections (as they should
 868              // remain uppercase). This must be done after the previous step
 869              // as that can add unescaped characters.
 870              $position = 0;
 871              $strlen = strlen($ihost);
 872              while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen) {
 873                  if ($ihost[$position] === '%') {
 874                      $position += 3;
 875                  } else {
 876                      $ihost[$position] = strtolower($ihost[$position]);
 877                      $position++;
 878                  }
 879              }
 880  
 881              $this->ihost = $ihost;
 882          }
 883  
 884          $this->scheme_normalization();
 885  
 886          return true;
 887      }
 888  
 889      /**
 890       * Set the port. Returns true on success, false on failure (if there are
 891       * any invalid characters).
 892       *
 893       * @param string|int|null $port
 894       * @return bool
 895       */
 896      public function set_port($port)
 897      {
 898          if ($port === null) {
 899              $this->port = null;
 900              return true;
 901          } elseif (strspn((string) $port, '0123456789') === strlen((string) $port)) {
 902              $this->port = (int) $port;
 903              $this->scheme_normalization();
 904              return true;
 905          }
 906  
 907          $this->port = null;
 908          return false;
 909      }
 910  
 911      /**
 912       * Set the ipath.
 913       *
 914       * @param string|null $ipath
 915       * @return bool
 916       */
 917      public function set_path(?string $ipath, bool $clear_cache = false)
 918      {
 919          static $cache;
 920          if ($clear_cache) {
 921              $cache = null;
 922              return false;
 923          }
 924          if (!$cache) {
 925              $cache = [];
 926          }
 927  
 928          $ipath = (string) $ipath;
 929  
 930          if (isset($cache[$ipath])) {
 931              $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
 932          } else {
 933              $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
 934              $removed = $this->remove_dot_segments($valid);
 935  
 936              $cache[$ipath] = [$valid, $removed];
 937              $this->ipath =  ($this->scheme !== null) ? $removed : $valid;
 938          }
 939  
 940          $this->scheme_normalization();
 941          return true;
 942      }
 943  
 944      /**
 945       * Set the iquery.
 946       *
 947       * @param string|null $iquery
 948       * @return bool
 949       */
 950      public function set_query(?string $iquery)
 951      {
 952          if ($iquery === null) {
 953              $this->iquery = null;
 954          } else {
 955              $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
 956              $this->scheme_normalization();
 957          }
 958          return true;
 959      }
 960  
 961      /**
 962       * Set the ifragment.
 963       *
 964       * @param string|null $ifragment
 965       * @return bool
 966       */
 967      public function set_fragment(?string $ifragment)
 968      {
 969          if ($ifragment === null) {
 970              $this->ifragment = null;
 971          } else {
 972              $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
 973              $this->scheme_normalization();
 974          }
 975          return true;
 976      }
 977  
 978      /**
 979       * Convert an IRI to a URI (or parts thereof)
 980       *
 981       * @param string $string
 982       * @return string
 983       */
 984      public function to_uri(string $string)
 985      {
 986          static $non_ascii;
 987          if (!$non_ascii) {
 988              $non_ascii = implode('', range("\x80", "\xFF"));
 989          }
 990  
 991          $position = 0;
 992          $strlen = strlen($string);
 993          while (($position += strcspn($string, $non_ascii, $position)) < $strlen) {
 994              $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1);
 995              $position += 3;
 996              $strlen += 2;
 997          }
 998  
 999          return $string;
1000      }
1001  
1002      /**
1003       * Get the complete IRI
1004       *
1005       * @return string|false
1006       */
1007      public function get_iri()
1008      {
1009          if (!$this->is_valid()) {
1010              return false;
1011          }
1012  
1013          $iri = '';
1014          if ($this->scheme !== null) {
1015              $iri .= $this->scheme . ':';
1016          }
1017          if (($iauthority = $this->get_iauthority()) !== null) {
1018              $iri .= '//' . $iauthority;
1019          }
1020          if ($this->ipath !== '') {
1021              $iri .= $this->ipath;
1022          } elseif (!empty($this->normalization[$this->scheme]['ipath']) && $iauthority !== null && $iauthority !== '') {
1023              $iri .= $this->normalization[$this->scheme]['ipath'];
1024          }
1025          if ($this->iquery !== null) {
1026              $iri .= '?' . $this->iquery;
1027          }
1028          if ($this->ifragment !== null) {
1029              $iri .= '#' . $this->ifragment;
1030          }
1031  
1032          return $iri;
1033      }
1034  
1035      /**
1036       * Get the complete URI
1037       *
1038       * @return string
1039       */
1040      public function get_uri()
1041      {
1042          return $this->to_uri((string) $this->get_iri());
1043      }
1044  
1045      /**
1046       * Get the complete iauthority
1047       *
1048       * @return ?string
1049       */
1050      protected function get_iauthority()
1051      {
1052          if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null) {
1053              $iauthority = '';
1054              if ($this->iuserinfo !== null) {
1055                  $iauthority .= $this->iuserinfo . '@';
1056              }
1057              if ($this->ihost !== null) {
1058                  $iauthority .= $this->ihost;
1059              }
1060              if ($this->port !== null && $this->port !== 0) {
1061                  $iauthority .= ':' . $this->port;
1062              }
1063              return $iauthority;
1064          }
1065  
1066          return null;
1067      }
1068  
1069      /**
1070       * Get the complete authority
1071       *
1072       * @return ?string
1073       */
1074      protected function get_authority()
1075      {
1076          $iauthority = $this->get_iauthority();
1077          if (is_string($iauthority)) {
1078              return $this->to_uri($iauthority);
1079          }
1080  
1081          return $iauthority;
1082      }
1083  }
1084  
1085  class_alias('SimplePie\IRI', 'SimplePie_IRI');


Generated : Thu Sep 18 08:20:05 2025 Cross-referenced by PHPXref