URL2.php 34 KB

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