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