1<?php 2/** 3 * Net_URL2, a class representing a URL as per RFC 3986. 4 * 5 * PHP version 5 6 * 7 * LICENSE: 8 * 9 * Copyright (c) 2007-2009, Peytz & Co. A/S 10 * All rights reserved. 11 * 12 * Redistribution and use in source and binary forms, with or without 13 * modification, are permitted provided that the following conditions 14 * are met: 15 * 16 * * Redistributions of source code must retain the above copyright 17 * notice, this list of conditions and the following disclaimer. 18 * * Redistributions in binary form must reproduce the above copyright 19 * notice, this list of conditions and the following disclaimer in 20 * the documentation and/or other materials provided with the distribution. 21 * * Neither the name of the Net_URL2 nor the names of its contributors may 22 * be used to endorse or promote products derived from this software 23 * without specific prior written permission. 24 * 25 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 26 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 27 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 28 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 29 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 30 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 31 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 32 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY 33 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 34 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 35 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 * 37 * @category Networking 38 * @package Net_URL2 39 * @author Christian Schmidt <schmidt@php.net> 40 * @copyright 2007-2009 Peytz & Co. A/S 41 * @license https://spdx.org/licenses/BSD-3-Clause BSD-3-Clause 42 * @version CVS: $Id$ 43 * @link https://tools.ietf.org/html/rfc3986 44 */ 45 46/** 47 * Represents a URL as per RFC 3986. 48 * 49 * @category Networking 50 * @package Net_URL2 51 * @author Christian Schmidt <schmidt@php.net> 52 * @copyright 2007-2009 Peytz & Co. A/S 53 * @license https://spdx.org/licenses/BSD-3-Clause BSD-3-Clause 54 * @version Release: 2.1.2 55 * @link https://pear.php.net/package/Net_URL2 56 */ 57class Net_URL2 58{ 59 /** 60 * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default 61 * is true. 62 */ 63 const OPTION_STRICT = 'strict'; 64 65 /** 66 * Represent arrays in query using PHP's [] notation. Default is true. 67 */ 68 const OPTION_USE_BRACKETS = 'use_brackets'; 69 70 /** 71 * Drop zero-based integer sequences in query using PHP's [] notation. Default 72 * is true. 73 */ 74 const OPTION_DROP_SEQUENCE = 'drop_sequence'; 75 76 /** 77 * URL-encode query variable keys. Default is true. 78 */ 79 const OPTION_ENCODE_KEYS = 'encode_keys'; 80 81 /** 82 * Query variable separators when parsing the query string. Every character 83 * is considered a separator. Default is "&". 84 */ 85 const OPTION_SEPARATOR_INPUT = 'input_separator'; 86 87 /** 88 * Query variable separator used when generating the query string. Default 89 * is "&". 90 */ 91 const OPTION_SEPARATOR_OUTPUT = 'output_separator'; 92 93 /** 94 * Default options corresponds to how PHP handles $_GET. 95 */ 96 private $_options = array( 97 self::OPTION_STRICT => true, 98 self::OPTION_USE_BRACKETS => true, 99 self::OPTION_DROP_SEQUENCE => true, 100 self::OPTION_ENCODE_KEYS => true, 101 self::OPTION_SEPARATOR_INPUT => '&', 102 self::OPTION_SEPARATOR_OUTPUT => '&', 103 ); 104 105 /** 106 * @var string|bool 107 */ 108 private $_scheme = false; 109 110 /** 111 * @var string|bool 112 */ 113 private $_userinfo = false; 114 115 /** 116 * @var string|bool 117 */ 118 private $_host = false; 119 120 /** 121 * @var string|bool 122 */ 123 private $_port = false; 124 125 /** 126 * @var string 127 */ 128 private $_path = ''; 129 130 /** 131 * @var string|bool 132 */ 133 private $_query = false; 134 135 /** 136 * @var string|bool 137 */ 138 private $_fragment = false; 139 140 /** 141 * Constructor. 142 * 143 * @param string $url an absolute or relative URL 144 * @param array $options an array of OPTION_xxx constants 145 * 146 * @uses self::parseUrl() 147 */ 148 public function __construct($url, array $options = array()) 149 { 150 foreach ($options as $optionName => $value) { 151 if (array_key_exists($optionName, $this->_options)) { 152 $this->_options[$optionName] = $value; 153 } 154 } 155 156 $this->parseUrl($url); 157 } 158 159 /** 160 * Magic Setter. 161 * 162 * This method will magically set the value of a private variable ($var) 163 * with the value passed as the args 164 * 165 * @param string $var The private variable to set. 166 * @param mixed $arg An argument of any type. 167 * 168 * @return void 169 */ 170 public function __set($var, $arg) 171 { 172 $method = 'set' . $var; 173 if (method_exists($this, $method)) { 174 $this->$method($arg); 175 } 176 } 177 178 /** 179 * Magic Getter. 180 * 181 * This is the magic get method to retrieve the private variable 182 * that was set by either __set() or it's setter... 183 * 184 * @param string $var The property name to retrieve. 185 * 186 * @return mixed $this->$var Either a boolean false if the 187 * property is not set or the value 188 * of the private property. 189 */ 190 public function __get($var) 191 { 192 $method = 'get' . $var; 193 if (method_exists($this, $method)) { 194 return $this->$method(); 195 } 196 197 return false; 198 } 199 200 /** 201 * Returns the scheme, e.g. "http" or "urn", or false if there is no 202 * scheme specified, i.e. if this is a relative URL. 203 * 204 * @return string|bool 205 */ 206 public function getScheme() 207 { 208 return $this->_scheme; 209 } 210 211 /** 212 * Sets the scheme, e.g. "http" or "urn". Specify false if there is no 213 * scheme specified, i.e. if this is a relative URL. 214 * 215 * @param string|bool $scheme e.g. "http" or "urn", or false if there is no 216 * scheme specified, i.e. if this is a relative 217 * URL 218 * 219 * @return $this 220 * @see getScheme 221 */ 222 public function setScheme($scheme) 223 { 224 $this->_scheme = $scheme; 225 return $this; 226 } 227 228 /** 229 * Returns the user part of the userinfo part (the part preceding the first 230 * ":"), or false if there is no userinfo part. 231 * 232 * @return string|bool 233 */ 234 public function getUser() 235 { 236 return $this->_userinfo !== false 237 ? preg_replace('(:.*$)', '', $this->_userinfo) 238 : false; 239 } 240 241 /** 242 * Returns the password part of the userinfo part (the part after the first 243 * ":"), or false if there is no userinfo part (i.e. the URL does not 244 * contain "@" in front of the hostname) or the userinfo part does not 245 * contain ":". 246 * 247 * @return string|bool 248 */ 249 public function getPassword() 250 { 251 return $this->_userinfo !== false 252 ? substr(strstr($this->_userinfo, ':'), 1) 253 : false; 254 } 255 256 /** 257 * Returns the userinfo part, or false if there is none, i.e. if the 258 * authority part does not contain "@". 259 * 260 * @return string|bool 261 */ 262 public function getUserinfo() 263 { 264 return $this->_userinfo; 265 } 266 267 /** 268 * Sets the userinfo part. If two arguments are passed, they are combined 269 * in the userinfo part as username ":" password. 270 * 271 * @param string|bool $userinfo userinfo or username 272 * @param string|bool $password optional password, or false 273 * 274 * @return $this 275 */ 276 public function setUserinfo($userinfo, $password = false) 277 { 278 if ($password !== false) { 279 $userinfo .= ':' . $password; 280 } 281 282 if ($userinfo !== false) { 283 $userinfo = $this->_encodeData($userinfo); 284 } 285 286 $this->_userinfo = $userinfo; 287 return $this; 288 } 289 290 /** 291 * Returns the host part, or false if there is no authority part, e.g. 292 * relative URLs. 293 * 294 * @return string|bool a hostname, an IP address, or false 295 */ 296 public function getHost() 297 { 298 return $this->_host; 299 } 300 301 /** 302 * Sets the host part. Specify false if there is no authority part, e.g. 303 * relative URLs. 304 * 305 * @param string|bool $host a hostname, an IP address, or false 306 * 307 * @return $this 308 */ 309 public function setHost($host) 310 { 311 $this->_host = $host; 312 return $this; 313 } 314 315 /** 316 * Returns the port number, or false if there is no port number specified, 317 * i.e. if the default port is to be used. 318 * 319 * @return string|bool 320 */ 321 public function getPort() 322 { 323 return $this->_port; 324 } 325 326 /** 327 * Sets the port number. Specify false if there is no port number specified, 328 * i.e. if the default port is to be used. 329 * 330 * @param string|bool $port a port number, or false 331 * 332 * @return $this 333 */ 334 public function setPort($port) 335 { 336 $this->_port = $port; 337 return $this; 338 } 339 340 /** 341 * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or 342 * false if there is no authority. 343 * 344 * @return string|bool 345 */ 346 public function getAuthority() 347 { 348 if (false === $this->_host) { 349 return false; 350 } 351 352 $authority = ''; 353 354 if (strlen($this->_userinfo)) { 355 $authority .= $this->_userinfo . '@'; 356 } 357 358 $authority .= $this->_host; 359 360 if ($this->_port !== false) { 361 $authority .= ':' . $this->_port; 362 } 363 364 return $authority; 365 } 366 367 /** 368 * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify 369 * false if there is no authority. 370 * 371 * @param string|bool $authority a hostname or an IP address, possibly 372 * with userinfo prefixed and port number 373 * appended, e.g. "foo:bar@example.org:81". 374 * 375 * @return $this 376 */ 377 public function setAuthority($authority) 378 { 379 $this->_userinfo = false; 380 $this->_host = false; 381 $this->_port = false; 382 383 if ('' === $authority) { 384 $this->_host = $authority; 385 return $this; 386 } 387 388 if (!preg_match('(^(([^@]*)@)?(.+?)(:(\d*))?$)', $authority, $matches)) { 389 return $this; 390 } 391 392 if ($matches[1]) { 393 $this->_userinfo = $this->_encodeData($matches[2]); 394 } 395 396 $this->_host = $matches[3]; 397 398 if (isset($matches[5]) && strlen($matches[5])) { 399 $this->_port = $matches[5]; 400 } 401 return $this; 402 } 403 404 /** 405 * Returns the path part (possibly an empty string). 406 * 407 * @return string 408 */ 409 public function getPath() 410 { 411 return $this->_path; 412 } 413 414 /** 415 * Sets the path part (possibly an empty string). 416 * 417 * @param string $path a path 418 * 419 * @return $this 420 */ 421 public function setPath($path) 422 { 423 $this->_path = $path; 424 return $this; 425 } 426 427 /** 428 * Returns the query string (excluding the leading "?"), or false if "?" 429 * is not present in the URL. 430 * 431 * @return string|bool 432 * @see getQueryVariables 433 */ 434 public function getQuery() 435 { 436 return $this->_query; 437 } 438 439 /** 440 * Sets the query string (excluding the leading "?"). Specify false if "?" 441 * is not present in the URL. 442 * 443 * @param string|bool $query a query string, e.g. "foo=1&bar=2" 444 * 445 * @return $this 446 * @see setQueryVariables 447 */ 448 public function setQuery($query) 449 { 450 $this->_query = $query; 451 return $this; 452 } 453 454 /** 455 * Returns the fragment name, or false if "#" is not present in the URL. 456 * 457 * @return string|bool 458 */ 459 public function getFragment() 460 { 461 return $this->_fragment; 462 } 463 464 /** 465 * Sets the fragment name. Specify false if "#" is not present in the URL. 466 * 467 * @param string|bool $fragment a fragment excluding the leading "#", or 468 * false 469 * 470 * @return $this 471 */ 472 public function setFragment($fragment) 473 { 474 $this->_fragment = $fragment; 475 return $this; 476 } 477 478 /** 479 * Returns the query string like an array as the variables would appear in 480 * $_GET in a PHP script. If the URL does not contain a "?", an empty array 481 * is returned. 482 * 483 * @return array 484 */ 485 public function getQueryVariables() 486 { 487 $separator = $this->getOption(self::OPTION_SEPARATOR_INPUT); 488 $encodeKeys = $this->getOption(self::OPTION_ENCODE_KEYS); 489 $useBrackets = $this->getOption(self::OPTION_USE_BRACKETS); 490 491 $return = array(); 492 493 for ($part = strtok($this->_query, $separator); 494 strlen($part); 495 $part = strtok($separator) 496 ) { 497 list($key, $value) = explode('=', $part, 2) + array(1 => ''); 498 499 if ($encodeKeys) { 500 $key = rawurldecode($key); 501 } 502 $value = rawurldecode($value); 503 504 if ($useBrackets) { 505 $return = $this->_queryArrayByKey($key, $value, $return); 506 } else { 507 if (isset($return[$key])) { 508 $return[$key] = (array) $return[$key]; 509 $return[$key][] = $value; 510 } else { 511 $return[$key] = $value; 512 } 513 } 514 } 515 516 return $return; 517 } 518 519 /** 520 * Parse a single query key=value pair into an existing php array 521 * 522 * @param string $key query-key 523 * @param string $value query-value 524 * @param array $array of existing query variables (if any) 525 * 526 * @return mixed 527 */ 528 private function _queryArrayByKey($key, $value, array $array = array()) 529 { 530 if (!strlen($key)) { 531 return $array; 532 } 533 534 $offset = $this->_queryKeyBracketOffset($key); 535 if ($offset === false) { 536 $name = $key; 537 } else { 538 $name = substr($key, 0, $offset); 539 } 540 541 if (!strlen($name)) { 542 return $array; 543 } 544 545 if (!$offset) { 546 // named value 547 $array[$name] = $value; 548 } else { 549 // array 550 $brackets = substr($key, $offset); 551 if (!isset($array[$name])) { 552 $array[$name] = null; 553 } 554 $array[$name] = $this->_queryArrayByBrackets( 555 $brackets, $value, $array[$name] 556 ); 557 } 558 559 return $array; 560 } 561 562 /** 563 * Parse a key-buffer to place value in array 564 * 565 * @param string $buffer to consume all keys from 566 * @param string $value to be set/add 567 * @param array $array to traverse and set/add value in 568 * 569 * @throws Exception 570 * @return array 571 */ 572 private function _queryArrayByBrackets($buffer, $value, array $array = null) 573 { 574 $entry = &$array; 575 576 for ($iteration = 0; strlen($buffer); $iteration++) { 577 $open = $this->_queryKeyBracketOffset($buffer); 578 if ($open !== 0) { 579 // Opening bracket [ must exist at offset 0, if not, there is 580 // no bracket to parse and the value dropped. 581 // if this happens in the first iteration, this is flawed, see 582 // as well the second exception below. 583 if ($iteration) { 584 break; 585 } 586 // @codeCoverageIgnoreStart 587 throw new Exception( 588 'Net_URL2 Internal Error: '. __METHOD__ .'(): ' . 589 'Opening bracket [ must exist at offset 0' 590 ); 591 // @codeCoverageIgnoreEnd 592 } 593 594 $close = strpos($buffer, ']', 1); 595 if (!$close) { 596 // this error condition should never be reached as this is a 597 // private method and bracket pairs are checked beforehand. 598 // See as well the first exception for the opening bracket. 599 // @codeCoverageIgnoreStart 600 throw new Exception( 601 'Net_URL2 Internal Error: '. __METHOD__ .'(): ' . 602 'Closing bracket ] must exist, not found' 603 ); 604 // @codeCoverageIgnoreEnd 605 } 606 607 $index = substr($buffer, 1, $close - 1); 608 if (strlen($index)) { 609 $entry = &$entry[$index]; 610 } else { 611 if (!is_array($entry)) { 612 $entry = array(); 613 } 614 $entry[] = &$new; 615 $entry = &$new; 616 unset($new); 617 } 618 $buffer = substr($buffer, $close + 1); 619 } 620 621 $entry = $value; 622 623 return $array; 624 } 625 626 /** 627 * Query-key has brackets ("...[]") 628 * 629 * @param string $key query-key 630 * 631 * @return bool|int offset of opening bracket, false if no brackets 632 */ 633 private function _queryKeyBracketOffset($key) 634 { 635 if (false !== $open = strpos($key, '[') 636 and false === strpos($key, ']', $open + 1) 637 ) { 638 $open = false; 639 } 640 641 return $open; 642 } 643 644 /** 645 * Sets the query string to the specified variable in the query string. 646 * 647 * @param array $array (name => value) array 648 * 649 * @return $this 650 */ 651 public function setQueryVariables(array $array) 652 { 653 if (!$array) { 654 $this->_query = false; 655 } else { 656 $this->_query = $this->buildQuery( 657 $array, 658 $this->getOption(self::OPTION_SEPARATOR_OUTPUT) 659 ); 660 } 661 return $this; 662 } 663 664 /** 665 * Sets the specified variable in the query string. 666 * 667 * @param string $name variable name 668 * @param mixed $value variable value 669 * 670 * @return $this 671 */ 672 public function setQueryVariable($name, $value) 673 { 674 $array = $this->getQueryVariables(); 675 $array[$name] = $value; 676 $this->setQueryVariables($array); 677 return $this; 678 } 679 680 /** 681 * Removes the specified variable from the query string. 682 * 683 * @param string $name a query string variable, e.g. "foo" in "?foo=1" 684 * 685 * @return void 686 */ 687 public function unsetQueryVariable($name) 688 { 689 $array = $this->getQueryVariables(); 690 unset($array[$name]); 691 $this->setQueryVariables($array); 692 } 693 694 /** 695 * Returns a string representation of this URL. 696 * 697 * @return string 698 */ 699 public function getURL() 700 { 701 // See RFC 3986, section 5.3 702 $url = ''; 703 704 if ($this->_scheme !== false) { 705 $url .= $this->_scheme . ':'; 706 } 707 708 $authority = $this->getAuthority(); 709 if ($authority === false && strtolower($this->_scheme) === 'file') { 710 $authority = ''; 711 } 712 713 $url .= $this->_buildAuthorityAndPath($authority, $this->_path); 714 715 if ($this->_query !== false) { 716 $url .= '?' . $this->_query; 717 } 718 719 if ($this->_fragment !== false) { 720 $url .= '#' . $this->_fragment; 721 } 722 723 return $url; 724 } 725 726 /** 727 * Put authority and path together, wrapping authority 728 * into proper separators/terminators. 729 * 730 * @param string|bool $authority authority 731 * @param string $path path 732 * 733 * @return string 734 */ 735 private function _buildAuthorityAndPath($authority, $path) 736 { 737 if ($authority === false) { 738 return $path; 739 } 740 741 $terminator = ($path !== '' && $path[0] !== '/') ? '/' : ''; 742 743 return '//' . $authority . $terminator . $path; 744 } 745 746 /** 747 * Returns a string representation of this URL. 748 * 749 * @return string 750 * @link https://php.net/language.oop5.magic#object.tostring 751 */ 752 public function __toString() 753 { 754 return $this->getURL(); 755 } 756 757 /** 758 * Returns a normalized string representation of this URL. This is useful 759 * for comparison of URLs. 760 * 761 * @return string 762 */ 763 public function getNormalizedURL() 764 { 765 $url = clone $this; 766 $url->normalize(); 767 return $url->getURL(); 768 } 769 770 /** 771 * Normalizes the URL 772 * 773 * See RFC 3986, Section 6. Normalization and Comparison 774 * 775 * @link https://tools.ietf.org/html/rfc3986#section-6 776 * 777 * @return void 778 */ 779 public function normalize() 780 { 781 // See RFC 3986, section 6 782 783 // Scheme is case-insensitive 784 if ($this->_scheme) { 785 $this->_scheme = strtolower($this->_scheme); 786 } 787 788 // Hostname is case-insensitive 789 if ($this->_host) { 790 $this->_host = strtolower($this->_host); 791 } 792 793 // Remove default port number for known schemes (RFC 3986, section 6.2.3) 794 if ('' === $this->_port 795 || $this->_port 796 && $this->_scheme 797 && $this->_port == getservbyname($this->_scheme, 'tcp') 798 ) { 799 $this->_port = false; 800 } 801 802 // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) 803 // Normalize percentage-encoded unreserved characters (section 6.2.2.2) 804 $fields = array(&$this->_userinfo, &$this->_host, &$this->_path, 805 &$this->_query, &$this->_fragment); 806 foreach ($fields as &$field) { 807 if ($field !== false) { 808 $field = $this->_normalize("$field"); 809 } 810 } 811 unset($field); 812 813 // Path segment normalization (RFC 3986, section 6.2.2.3) 814 $this->_path = self::removeDotSegments($this->_path); 815 816 // Scheme based normalization (RFC 3986, section 6.2.3) 817 if (false !== $this->_host && '' === $this->_path) { 818 $this->_path = '/'; 819 } 820 821 // path should start with '/' if there is authority (section 3.3.) 822 if (strlen($this->getAuthority()) 823 && strlen($this->_path) 824 && $this->_path[0] !== '/' 825 ) { 826 $this->_path = '/' . $this->_path; 827 } 828 } 829 830 /** 831 * Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) 832 * Normalize percentage-encoded unreserved characters (section 6.2.2.2) 833 * 834 * @param string|array $mixed string or array of strings to normalize 835 * 836 * @return string|array 837 * @see normalize 838 * @see _normalizeCallback() 839 */ 840 private function _normalize($mixed) 841 { 842 return preg_replace_callback( 843 '((?:%[0-9a-fA-Z]{2})+)', array($this, '_normalizeCallback'), 844 $mixed 845 ); 846 } 847 848 /** 849 * Callback for _normalize() of %XX percentage-encodings 850 * 851 * @param array $matches as by preg_replace_callback 852 * 853 * @return string 854 * @see normalize 855 * @see _normalize 856 * @SuppressWarnings(PHPMD.UnusedPrivateMethod) 857 */ 858 private function _normalizeCallback($matches) 859 { 860 return self::urlencode(urldecode($matches[0])); 861 } 862 863 /** 864 * Returns whether this instance represents an absolute URL. 865 * 866 * @return bool 867 */ 868 public function isAbsolute() 869 { 870 return (bool) $this->_scheme; 871 } 872 873 /** 874 * Returns an Net_URL2 instance representing an absolute URL relative to 875 * this URL. 876 * 877 * @param Net_URL2|string $reference relative URL 878 * 879 * @throws Exception 880 * @return $this 881 */ 882 public function resolve($reference) 883 { 884 if (!$reference instanceof Net_URL2) { 885 $reference = new self($reference); 886 } 887 if (!$reference->_isFragmentOnly() && !$this->isAbsolute()) { 888 throw new Exception( 889 'Base-URL must be absolute if reference is not fragment-only' 890 ); 891 } 892 893 // A non-strict parser may ignore a scheme in the reference if it is 894 // identical to the base URI's scheme. 895 if (!$this->getOption(self::OPTION_STRICT) 896 && $reference->_scheme == $this->_scheme 897 ) { 898 $reference->_scheme = false; 899 } 900 901 $target = new self(''); 902 if ($reference->_scheme !== false) { 903 $target->_scheme = $reference->_scheme; 904 $target->setAuthority($reference->getAuthority()); 905 $target->_path = self::removeDotSegments($reference->_path); 906 $target->_query = $reference->_query; 907 } else { 908 $authority = $reference->getAuthority(); 909 if ($authority !== false) { 910 $target->setAuthority($authority); 911 $target->_path = self::removeDotSegments($reference->_path); 912 $target->_query = $reference->_query; 913 } else { 914 if ($reference->_path == '') { 915 $target->_path = $this->_path; 916 if ($reference->_query !== false) { 917 $target->_query = $reference->_query; 918 } else { 919 $target->_query = $this->_query; 920 } 921 } else { 922 if (substr($reference->_path, 0, 1) == '/') { 923 $target->_path = self::removeDotSegments($reference->_path); 924 } else { 925 // Merge paths (RFC 3986, section 5.2.3) 926 if ($this->_host !== false && $this->_path == '') { 927 $target->_path = '/' . $reference->_path; 928 } else { 929 $i = strrpos($this->_path, '/'); 930 if ($i !== false) { 931 $target->_path = substr($this->_path, 0, $i + 1); 932 } 933 $target->_path .= $reference->_path; 934 } 935 $target->_path = self::removeDotSegments($target->_path); 936 } 937 $target->_query = $reference->_query; 938 } 939 $target->setAuthority($this->getAuthority()); 940 } 941 $target->_scheme = $this->_scheme; 942 } 943 944 $target->_fragment = $reference->_fragment; 945 946 return $target; 947 } 948 949 /** 950 * URL is fragment-only 951 * 952 * @SuppressWarnings(PHPMD.UnusedPrivateMethod) 953 * @return bool 954 */ 955 private function _isFragmentOnly() 956 { 957 return ( 958 $this->_fragment !== false 959 && $this->_query === false 960 && $this->_path === '' 961 && $this->_port === false 962 && $this->_host === false 963 && $this->_userinfo === false 964 && $this->_scheme === false 965 ); 966 } 967 968 /** 969 * Removes dots as described in RFC 3986, section 5.2.4, e.g. 970 * "/foo/../bar/baz" => "/bar/baz" 971 * 972 * @param string $path a path 973 * 974 * @return string a path 975 */ 976 public static function removeDotSegments($path) 977 { 978 $path = (string) $path; 979 $output = ''; 980 981 // Make sure not to be trapped in an infinite loop due to a bug in this 982 // method 983 $loopLimit = 256; 984 $j = 0; 985 while ('' !== $path && $j++ < $loopLimit) { 986 if (substr($path, 0, 2) === './') { 987 // Step 2.A 988 $path = substr($path, 2); 989 } elseif (substr($path, 0, 3) === '../') { 990 // Step 2.A 991 $path = substr($path, 3); 992 } elseif (substr($path, 0, 3) === '/./' || $path === '/.') { 993 // Step 2.B 994 $path = '/' . substr($path, 3); 995 } elseif (substr($path, 0, 4) === '/../' || $path === '/..') { 996 // Step 2.C 997 $path = '/' . substr($path, 4); 998 $i = strrpos($output, '/'); 999 $output = $i === false ? '' : substr($output, 0, $i); 1000 } elseif ($path === '.' || $path === '..') { 1001 // Step 2.D 1002 $path = ''; 1003 } else { 1004 // Step 2.E 1005 $i = strpos($path, '/', $path[0] === '/'); 1006 if ($i === false) { 1007 $output .= $path; 1008 $path = ''; 1009 break; 1010 } 1011 $output .= substr($path, 0, $i); 1012 $path = substr($path, $i); 1013 } 1014 } 1015 1016 if ($path !== '') { 1017 $message = sprintf( 1018 'Unable to remove dot segments; hit loop limit %d (left: %s)', 1019 $j, var_export($path, true) 1020 ); 1021 trigger_error($message, E_USER_WARNING); 1022 } 1023 1024 return $output; 1025 } 1026 1027 /** 1028 * Percent-encodes all non-alphanumeric characters except these: _ . - ~ 1029 * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP 1030 * 5.2.x and earlier. 1031 * 1032 * @param string $string string to encode 1033 * 1034 * @return string 1035 */ 1036 public static function urlencode($string) 1037 { 1038 $encoded = rawurlencode($string); 1039 1040 // This is only necessary in PHP < 5.3. 1041 $encoded = str_replace('%7E', '~', $encoded); 1042 return $encoded; 1043 } 1044 1045 /** 1046 * Returns a Net_URL2 instance representing the canonical URL of the 1047 * currently executing PHP script. 1048 * 1049 * @throws Exception 1050 * @return string 1051 */ 1052 public static function getCanonical() 1053 { 1054 if (!isset($_SERVER['REQUEST_METHOD'])) { 1055 // ALERT - no current URL 1056 throw new Exception('Script was not called through a webserver'); 1057 } 1058 1059 // Begin with a relative URL 1060 $url = new self($_SERVER['PHP_SELF']); 1061 $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; 1062 $url->_host = $_SERVER['SERVER_NAME']; 1063 $port = $_SERVER['SERVER_PORT']; 1064 if ($url->_scheme == 'http' && $port != 80 1065 || $url->_scheme == 'https' && $port != 443 1066 ) { 1067 $url->_port = $port; 1068 } 1069 return $url; 1070 } 1071 1072 /** 1073 * Returns the URL used to retrieve the current request. 1074 * 1075 * @return string 1076 */ 1077 public static function getRequestedURL() 1078 { 1079 return self::getRequested()->getUrl(); 1080 } 1081 1082 /** 1083 * Returns a Net_URL2 instance representing the URL used to retrieve the 1084 * current request. 1085 * 1086 * @throws Exception 1087 * @return $this 1088 */ 1089 public static function getRequested() 1090 { 1091 if (!isset($_SERVER['REQUEST_METHOD'])) { 1092 // ALERT - no current URL 1093 throw new Exception('Script was not called through a webserver'); 1094 } 1095 1096 // Begin with a relative URL 1097 $url = new self($_SERVER['REQUEST_URI']); 1098 $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; 1099 // Set host and possibly port 1100 $url->setAuthority($_SERVER['HTTP_HOST']); 1101 return $url; 1102 } 1103 1104 /** 1105 * Returns the value of the specified option. 1106 * 1107 * @param string $optionName The name of the option to retrieve 1108 * 1109 * @return mixed 1110 */ 1111 public function getOption($optionName) 1112 { 1113 return isset($this->_options[$optionName]) 1114 ? $this->_options[$optionName] : false; 1115 } 1116 1117 /** 1118 * A simple version of http_build_query in userland. The encoded string is 1119 * percentage encoded according to RFC 3986. 1120 * 1121 * @param array $data An array, which has to be converted into 1122 * QUERY_STRING. Anything is possible. 1123 * @param string $separator Separator {@link self::OPTION_SEPARATOR_OUTPUT} 1124 * @param string $key For stacked values (arrays in an array). 1125 * 1126 * @return string 1127 */ 1128 protected function buildQuery(array $data, $separator, $key = null) 1129 { 1130 $query = array(); 1131 $drop_names = ( 1132 $this->_options[self::OPTION_DROP_SEQUENCE] === true 1133 && array_keys($data) === array_keys(array_values($data)) 1134 ); 1135 foreach ($data as $name => $value) { 1136 if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) { 1137 $name = rawurlencode($name); 1138 } 1139 if ($key !== null) { 1140 if ($this->getOption(self::OPTION_USE_BRACKETS) === true) { 1141 $drop_names && $name = ''; 1142 $name = $key . '[' . $name . ']'; 1143 } else { 1144 $name = $key; 1145 } 1146 } 1147 if (is_array($value)) { 1148 $query[] = $this->buildQuery($value, $separator, $name); 1149 } else { 1150 $query[] = $name . '=' . rawurlencode($value); 1151 } 1152 } 1153 return implode($separator, $query); 1154 } 1155 1156 /** 1157 * This method uses a regex to parse the url into the designated parts. 1158 * 1159 * @param string $url URL 1160 * 1161 * @return void 1162 * @uses self::$_scheme, self::setAuthority(), self::$_path, self::$_query, 1163 * self::$_fragment 1164 * @see __construct 1165 */ 1166 protected function parseUrl($url) 1167 { 1168 // The regular expression is copied verbatim from RFC 3986, appendix B. 1169 // The expression does not validate the URL but matches any string. 1170 preg_match( 1171 '(^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)', 1172 $url, $matches 1173 ); 1174 1175 // "path" is always present (possibly as an empty string); the rest 1176 // are optional. 1177 $this->_scheme = !empty($matches[1]) ? $matches[2] : false; 1178 $this->setAuthority(!empty($matches[3]) ? $matches[4] : false); 1179 $this->_path = $this->_encodeData($matches[5]); 1180 $this->_query = !empty($matches[6]) 1181 ? $this->_encodeData($matches[7]) 1182 : false 1183 ; 1184 $this->_fragment = !empty($matches[8]) ? $matches[9] : false; 1185 } 1186 1187 /** 1188 * Encode characters that might have been forgotten to encode when passing 1189 * in an URL. Applied onto Userinfo, Path and Query. 1190 * 1191 * @param string $url URL 1192 * 1193 * @return string 1194 * @see parseUrl 1195 * @see setAuthority 1196 * @link https://pear.php.net/bugs/bug.php?id=20425 1197 */ 1198 private function _encodeData($url) 1199 { 1200 return preg_replace_callback( 1201 '([\x-\x20\x22\x3C\x3E\x7F-\xFF]+)', 1202 array($this, '_encodeCallback'), $url 1203 ); 1204 } 1205 1206 /** 1207 * callback for encoding character data 1208 * 1209 * @param array $matches Matches 1210 * 1211 * @return string 1212 * @see _encodeData 1213 * @SuppressWarnings(PHPMD.UnusedPrivateMethod) 1214 */ 1215 private function _encodeCallback(array $matches) 1216 { 1217 return rawurlencode($matches[0]); 1218 } 1219} 1220