00001 <?php
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00017 class GenericUri implements Stringable
00018 {
00019 const CHARS_UNRESERVED = 'a-z0-9-._~';
00020 const CHARS_SUBDELIMS = '!$&\'()*+,;=';
00021 const PATTERN_PCTENCODED = '%[0-9a-f][0-9a-f]';
00022
00023 protected $scheme = null;
00024
00025 protected $userInfo = null;
00026 protected $host = null;
00027 protected $port = null;
00028
00029 protected $path = null;
00030 protected $query = null;
00031 protected $fragment = null;
00032
00036 public static function create()
00037 {
00038 return new self;
00039 }
00040
00044 final public function parse($uri, $guessClass = false)
00045 {
00046 $schemePattern = '([^:/?#]+):';
00047 $authorityPattern = '(//([^/?#]*))';
00048 $restPattern = '([^?#]*)(\?([^#]*))?(#(.*))?';
00049 $matches = array();
00050
00051 if (
00052 $guessClass
00053 && ($knownSubSchemes = $this->getKnownSubSchemes())
00054 && preg_match("~^{$schemePattern}~", $uri, $matches)
00055 && isset($knownSubSchemes[strtolower($matches[1])])
00056 )
00057 $class = $knownSubSchemes[strtolower($matches[1])];
00058 else
00059 $class = get_class($this);
00060
00061 $result = new $class;
00062
00063 if ($result instanceof Url)
00064 $pattern = "({$schemePattern}{$authorityPattern})?";
00065 elseif ($result instanceof Urn)
00066 $pattern = "({$schemePattern})?";
00067 else
00068 $pattern = "({$schemePattern})?{$authorityPattern}?";
00069
00070 $pattern = "~^{$pattern}{$restPattern}$~";
00071
00072 if (!preg_match($pattern, $uri, $matches))
00073 throw new WrongArgumentException('not well-formed URI');
00074
00075 array_shift($matches);
00076
00077 if ($matches[0])
00078 $result->setScheme($matches[1]);
00079
00080 array_shift($matches);
00081 array_shift($matches);
00082
00083 if (!($result instanceof Urn)) {
00084 if ($matches[0])
00085 $result->setAuthority($matches[1]);
00086
00087 array_shift($matches);
00088 array_shift($matches);
00089 }
00090
00091 $result->setPath($matches[0]);
00092
00093 if (!empty($matches[1]))
00094 $result->setQuery($matches[2]);
00095
00096 if (!empty($matches[3]))
00097 $result->setFragment($matches[4]);
00098
00099 return $result;
00100 }
00101
00106 final public function transform(GenericUri $reference, $strict = true)
00107 {
00108 if ($this->getScheme() === null)
00109 throw new WrongStateException(
00110 'URI without scheme cannot be a base URI'
00111 );
00112
00113 if (
00114 $reference->getScheme() !== ($strict ? null : $this->getScheme())
00115 ) {
00116 $class = get_class($reference);
00117 $result = new $class;
00118
00119 $result->
00120 setScheme($reference->getScheme())->
00121 setUserInfo($reference->getUserInfo())->
00122 setHost($reference->getHost())->
00123 setPort($reference->getPort())->
00124 setPath(self::removeDotSegments($reference->getPath()))->
00125 setQuery($reference->getQuery());
00126 } else {
00127 $result = new $this;
00128
00129 $result->setScheme($this->getScheme());
00130
00131 if ($reference->getAuthority() !== null) {
00132 $result->
00133 setUserInfo($reference->getUserInfo())->
00134 setHost($reference->getHost())->
00135 setPort($reference->getPort())->
00136 setPath(self::removeDotSegments($reference->getPath()))->
00137 setQuery($reference->getQuery());
00138 } else {
00139 $result->
00140 setUserInfo($this->getUserInfo())->
00141 setHost($this->getHost())->
00142 setPort($this->getPort());
00143
00144 $path = $reference->getPath();
00145
00146 if (!$path) {
00147 $result->
00148 setPath($this->getPath())->
00149 setQuery(
00150 $reference->getQuery() !== null
00151 ? $reference->getQuery()
00152 : $this->getQuery()
00153 );
00154 } else {
00155 $result->setQuery($reference->getQuery());
00156
00157 if ($path[0] == '/')
00158 $result->setPath($path);
00159 else
00160 $result->setPath(
00161 self::removeDotSegments(
00162 self::mergePath($reference->getPath())
00163 )
00164 );
00165 }
00166 }
00167 }
00168
00169 $result->setFragment($reference->getFragment());
00170
00171 return $result;
00172 }
00173
00174 public function getKnownSubSchemes()
00175 {
00176 return array_merge(
00177 Urn::create()->getKnownSubSchemes(),
00178 Url::create()->getKnownSubSchemes()
00179 );
00180 }
00181
00185 public function setScheme($scheme)
00186 {
00187 $this->scheme = $scheme;
00188
00189 return $this;
00190 }
00191
00192 public function getScheme()
00193 {
00194 return $this->scheme;
00195 }
00196
00200 public function setUserInfo($userInfo)
00201 {
00202 $this->userInfo = $userInfo;
00203
00204 return $this;
00205 }
00206
00207 public function getUserInfo()
00208 {
00209 return $this->userInfo;
00210 }
00211
00215 public function setHost($host)
00216 {
00217 $this->host = $host;
00218
00219 return $this;
00220 }
00221
00222 public function getHost()
00223 {
00224 return $this->host;
00225 }
00226
00230 public function setPort($port)
00231 {
00232 $this->port = $port;
00233
00234 return $this;
00235 }
00236
00237 public function getPort()
00238 {
00239 return $this->port;
00240 }
00241
00245 public function setPath($path)
00246 {
00247 $this->path = $path;
00248
00249 return $this;
00250 }
00251
00252 public function getPath()
00253 {
00254 return $this->path;
00255 }
00256
00260 public function setQuery($query)
00261 {
00262 $this->query = $query;
00263
00264 return $this;
00265 }
00266
00270 public function appendQuery($string, $separator = '&')
00271 {
00272 $query = $this->query;
00273
00274 if ($query)
00275 $query .= $separator;
00276
00277 $query .= $string;
00278
00279 $this->setQuery($query);
00280
00281 return $this;
00282 }
00283
00284 public function getQuery()
00285 {
00286 return $this->query;
00287 }
00288
00292 public function setFragment($fragment)
00293 {
00294 $this->fragment = $fragment;
00295
00296 return $this;
00297 }
00298
00299 public function getFragment()
00300 {
00301 return $this->fragment;
00302 }
00303
00307 public function setAuthority($authority)
00308 {
00309 $authorityPattern = '~^(([^@]*)@)?((\[.+\])|([^:]*))(:(.*))?$~';
00310 $authorityMatches = array();
00311
00312 if (
00313 !preg_match(
00314 $authorityPattern, $authority, $authorityMatches
00315 )
00316 )
00317 throw new WrongArgumentException(
00318 'not well-formed authority part'
00319 );
00320
00321 if ($authorityMatches[1])
00322 $this->setUserInfo($authorityMatches[2]);
00323
00324 $this->setHost($authorityMatches[3]);
00325
00326 if (!empty($authorityMatches[6]))
00327 $this->setPort($authorityMatches[7]);
00328
00329 return $this;
00330 }
00331
00332 public function getAuthority()
00333 {
00334 $result = null;
00335
00336 if ($this->userInfo !== null)
00337 $result .= $this->userInfo.'@';
00338
00339 if ($this->host !== null)
00340 $result .= $this->host;
00341
00342 if ($this->port !== null)
00343 $result .= ':'.$this->port;
00344
00345 return $result;
00346 }
00347
00348 public function setSchemeSpecificPart($schemeSpecificPart)
00349 {
00350 throw new UnsupportedMethodException('use parse() instead');
00351 }
00352
00353 public function getSchemeSpecificPart()
00354 {
00355 $result = null;
00356
00357 $authority = $this->getAuthority();
00358
00359 if ($authority !== null)
00360 $result .= '//'.$authority;
00361
00362 $result .= $this->path;
00363
00364 if ($this->query !== null)
00365 $result .= '?'.$this->query;
00366
00367 if ($this->fragment !== null)
00368 $result .= '#'.$this->fragment;
00369
00370 return $result;
00371 }
00372
00373 public function toString()
00374 {
00375 $result = null;
00376
00377 if ($this->scheme !== null)
00378 $result .= $this->scheme.':';
00379
00380 $result .= $this->getSchemeSpecificPart();
00381
00382 return $result;
00383 }
00384
00385 public function toStringFromRoot()
00386 {
00387 $result = $this->path;
00388
00389 if ($this->query !== null)
00390 $result .= '?'.$this->query;
00391
00392 if ($this->fragment !== null)
00393 $result .= '#'.$this->fragment;
00394
00395 return $result;
00396 }
00397
00398 public function isValid()
00399 {
00400 return
00401 $this->isValidScheme()
00402 && $this->isValidUserInfo()
00403 && $this->isValidHost()
00404 && $this->isValidPort()
00405 && $this->isValidPath()
00406 && $this->isValidQuery()
00407 && $this->isValidFragment();
00408 }
00409
00410 public function isValidScheme()
00411 {
00412
00413 return (
00414 $this->scheme === null
00415 || preg_match('~^[a-z][-+.a-z0-9]*$~i', $this->scheme) == 1
00416 );
00417 }
00418
00419 public function isValidUserInfo()
00420 {
00421
00422 if (!$this->userInfo)
00423 return true;
00424
00425 $charPattern = $this->userInfoCharPattern();
00426
00427 return (preg_match("/^$charPattern*$/i", $this->userInfo) == 1);
00428 }
00429
00430 public function isValidHost()
00431 {
00432
00433 if (empty($this->host))
00434 return true;
00435
00436 $decOctet =
00437 '(\d)|'
00438 .'([1-9]\d)|'
00439 .'(1\d\d)|'
00440 .'(2[0-4]\d)|'
00441 .'(25[0-5])';
00442
00443 $ipV4Address = "($decOctet)\.($decOctet)\.($decOctet)\.($decOctet)";
00444
00445 $hexdig = '[0-9a-f]';
00446
00447 $h16 = "$hexdig{1,4}";
00448 $ls32 = "(($h16:$h16)|($ipV4Address))";
00449
00450 $ipV6Address =
00451 " ( ($h16:){6} $ls32)"
00452 ."|( ::($h16:){5} $ls32)"
00453 ."|( ($h16)? ::($h16:){4} $ls32)"
00454 ."|( (($h16:){0,1} $h16)? ::($h16:){3} $ls32)"
00455 ."|( (($h16:){0,2} $h16)? ::($h16:){2} $ls32)"
00456 ."|( (($h16:){0,3} $h16)? :: $h16: $ls32)"
00457 ."|( (($h16:){0,4} $h16)? :: $ls32)"
00458 ."|( (($h16:){0,5} $h16)? :: $h16 )"
00459 ."|( (($h16:){0,6} $h16)? :: )";
00460
00461 $unreserved = self::CHARS_UNRESERVED;
00462 $subDelims = self::CHARS_SUBDELIMS;
00463
00464 $ipVFutureAddress =
00465 "v$hexdig+\.[{$unreserved}{$subDelims}:]+";
00466
00467 if (
00468 preg_match(
00469 "/^\[(($ipV6Address)|($ipVFutureAddress))\]$/ix",
00470 $this->host
00471 )
00472 )
00473 return true;
00474
00475 if (preg_match("/^$ipV4Address$/i", $this->host)) {
00476 return true;
00477 }
00478
00479 return $this->isValidHostName();
00480 }
00481
00482 public function isValidPort()
00483 {
00484
00485 if (!$this->port)
00486 return true;
00487
00488 if (!preg_match('~^\d*$~', $this->port))
00489 return false;
00490
00491 return ($this->port > 0 && $this->port <= 65535);
00492 }
00493
00494 public function isValidPath()
00495 {
00496 $charPattern = $this->segmentCharPattern();
00497
00498 if (
00499 !preg_match(
00500 "/^($charPattern+)?"
00501 ."(\/$charPattern*)*$/i",
00502 $this->path
00503 )
00504 )
00505 return false;
00506
00507 if ($this->getAuthority() !== null) {
00508
00509 if (empty($this->path) || $this->path[0] == '/')
00510 return true;
00511
00512 } elseif ($this->path && $this->path[0] == '/') {
00513
00514 if ($this->path == '/' || $this->path[1] != '/')
00515 return true;
00516
00517 } elseif ($this->scheme === null && $this->path) {
00518
00519
00520 $segments = explode('/', $this->path);
00521
00522 if (strpos($segments[0], ':') === false)
00523 return true;
00524
00525 } elseif ($this->path) {
00526
00527 if ($this->path[0] != '/')
00528 return true;
00529
00530 } elseif (!$this->path) {
00531
00532 return true;
00533 }
00534
00535 return false;
00536 }
00537
00538 public function isValidQuery()
00539 {
00540
00541 return $this->isValidFragmentOrQuery($this->query);
00542 }
00543
00544 public function isValidFragment()
00545 {
00546
00547 return $this->isValidFragmentOrQuery($this->fragment);
00548 }
00549
00550 public function isAbsolute()
00551 {
00552 return ($this->scheme !== null);
00553 }
00554
00555 public function isRelative()
00556 {
00557 return ($this->scheme === null);
00558 }
00559
00560 protected function isValidHostName()
00561 {
00562 $charPattern = $this->hostNameCharPattern();
00563
00564 return (
00565 preg_match(
00566 "/^$charPattern*$/i",
00567 $this->host
00568 ) == 1
00569 );
00570 }
00571
00572 protected function charPattern(
00573 $extraChars = null, $pctEncodedPattern = true
00574 )
00575 {
00576 $unreserved = self::CHARS_UNRESERVED;
00577 $subDelims = self::CHARS_SUBDELIMS;
00578 $pctEncoded = self::PATTERN_PCTENCODED;
00579
00580 $result = "{$unreserved}{$subDelims}$extraChars";
00581
00582 if ($pctEncodedPattern)
00583 $result = "(([{$result}])|({$pctEncoded}))";
00584
00585 return $result;
00586 }
00587
00588 protected function userInfoCharPattern($pctEncoded = true)
00589 {
00590 return $this->charPattern(':', $pctEncoded);
00591 }
00592
00593 protected function hostNameCharPattern($pctEncoded = true)
00594 {
00595 return $this->charPattern(null, $pctEncoded);
00596 }
00597
00598 protected function segmentCharPattern($pctEncoded = true)
00599 {
00600 return $this->charPattern(':@', $pctEncoded);
00601 }
00602
00603 protected function fragmentOrQueryCharPattern($pctEncoded = true)
00604 {
00605 return $this->charPattern(':@\/?', $pctEncoded);
00606 }
00607
00608 private function isValidFragmentOrQuery($string)
00609 {
00610 $charPattern = $this->fragmentOrQueryCharPattern();
00611
00612 return (preg_match("/^$charPattern*$/i", $string) == 1);
00613 }
00614
00615 private static function removeDotSegments($path)
00616 {
00617 $segments = array();
00618
00619 while ($path) {
00620 if (strpos($path, '../') === 0) {
00621 $path = substr($path, 3);
00622
00623 } elseif (strpos($path, './') === 0) {
00624 $path = substr($path, 2);
00625
00626 } elseif (strpos($path, '/./') === 0) {
00627 $path = substr($path, 2);
00628
00629 } elseif ($path == '/.') {
00630 $path = '/';
00631
00632 } elseif (strpos($path, '/../') === 0) {
00633 $path = substr($path, 3);
00634
00635 if ($segments) {
00636 array_pop($segments);
00637 }
00638
00639 } elseif ($path == '/..') {
00640 $path = '/';
00641
00642 if ($segments) {
00643 array_pop($segments);
00644 }
00645
00646 } elseif (($path == '..') || ($path == '.')) {
00647 $path = null;
00648
00649 } else {
00650 $i = 0;
00651
00652 if ($path[0] == '/')
00653 $i = 1;
00654
00655 $i = strpos($path, '/', $i);
00656
00657 if ($i === false)
00658 $i = strlen($path);
00659
00660 $segments[] = substr($path, 0, $i);
00661
00662 $path = substr($path, $i);
00663 }
00664 }
00665
00666 return implode('', $segments);
00667 }
00668
00669 private function mergePath($path)
00670 {
00671 if ($this->getAuthority() !== null && !$this->getPath())
00672 return '/'.$path;
00673
00674 $segments = explode('/', $this->path);
00675
00676 array_pop($segments);
00677
00678 return implode('/', $segments).'/'.$path;
00679 }
00680
00684 public function normalize()
00685 {
00686
00687 if ($this->getScheme() !== null)
00688 $this->setScheme(mb_strtolower($this->getScheme()));
00689
00690
00691 $this->
00692 setHost(
00693 $this->normalizePercentEncoded(
00694 $this->getHost(), $this->hostNameCharPattern(false)
00695 )
00696 )->
00697 setUserInfo(
00698 $this->normalizePercentEncoded(
00699 $this->getUserInfo(), $this->userInfoCharPattern(false)
00700 )
00701 )->
00702 setPath(
00703 self::removeDotSegments(
00704 $this->normalizePercentEncoded(
00705 $this->getPath(),
00706 '\/'.$this->segmentCharPattern(false)
00707 )
00708 )
00709 )->
00710 setQuery(
00711 $this->normalizePercentEncoded(
00712 $this->getQuery(),
00713 $this->fragmentOrQueryCharPattern(false)
00714 )
00715 )->
00716 setFragment(
00717 $this->normalizePercentEncoded(
00718 $this->getFragment(),
00719 $this->fragmentOrQueryCharPattern(false)
00720 )
00721 );
00722
00723
00724 if ($this->getHost() !== null)
00725 $this->setHost(mb_strtolower($this->getHost()));
00726
00727 return $this;
00728 }
00729
00730 private function normalizePercentEncoded(
00731 $string, $unreservedPartChars
00732 )
00733 {
00734 if ($string === null)
00735 return null;
00736
00737 $result = preg_replace_callback(
00738 '/(('.self::PATTERN_PCTENCODED.')|(.))/sui',
00739 array(
00740 PercentEncodingNormalizator::create()->
00741 setUnreservedPartChars($unreservedPartChars),
00742 'normalize'
00743 ),
00744 $string
00745 );
00746
00747 return $result;
00748 }
00749 }
00750
00754 final class PercentEncodingNormalizator
00755 {
00756 private $unreservedPartChars = null;
00757
00761 public static function create()
00762 {
00763 return new self;
00764 }
00765
00769 public function setUnreservedPartChars($unreservedPartChars)
00770 {
00771 $this->unreservedPartChars = $unreservedPartChars;
00772 return $this;
00773 }
00774
00775 public function normalize($matched)
00776 {
00777 $char = $matched[0];
00778 if (mb_strlen($char) == 1) {
00779 if (
00780 !preg_match(
00781 '/^['.$this->unreservedPartChars.']$/',
00782 $char
00783 )
00784 )
00785 $char = rawurlencode($char);
00786 } else {
00787 if (
00788 preg_match(
00789 '/^['.GenericUri::CHARS_UNRESERVED.']$/',
00790 rawurldecode($char)
00791 )
00792 )
00793 $char = rawurldecode($char);
00794 else
00795 $char = strtoupper($char);
00796 }
00797 return $char;
00798 }
00799 }
00800 ?>