]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Net/URL2.php
Possible hack for tags from private dents in public profile or wrong scope (both...
[quix0rs-gnu-social.git] / extlib / Net / URL2.php
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   http://www.opensource.org/licenses/bsd-license.php New BSD License
42  * @version   CVS: $Id: URL2.php 309223 2011-03-14 14:26:32Z till $
43  * @link      http://www.rfc-editor.org/rfc/rfc3986.txt
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   http://www.opensource.org/licenses/bsd-license.php New BSD License
54  * @version   Release: @package_version@
55  * @link      http://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      * URL-encode query variable keys. Default is true.
72      */
73     const OPTION_ENCODE_KEYS = 'encode_keys';
74
75     /**
76      * Query variable separators when parsing the query string. Every character
77      * is considered a separator. Default is "&".
78      */
79     const OPTION_SEPARATOR_INPUT = 'input_separator';
80
81     /**
82      * Query variable separator used when generating the query string. Default
83      * is "&".
84      */
85     const OPTION_SEPARATOR_OUTPUT = 'output_separator';
86
87     /**
88      * Default options corresponds to how PHP handles $_GET.
89      */
90     private $_options = array(
91         self::OPTION_STRICT           => true,
92         self::OPTION_USE_BRACKETS     => true,
93         self::OPTION_ENCODE_KEYS      => true,
94         self::OPTION_SEPARATOR_INPUT  => '&',
95         self::OPTION_SEPARATOR_OUTPUT => '&',
96         );
97
98     /**
99      * @var  string|bool
100      */
101     private $_scheme = false;
102
103     /**
104      * @var  string|bool
105      */
106     private $_userinfo = false;
107
108     /**
109      * @var  string|bool
110      */
111     private $_host = false;
112
113     /**
114      * @var  string|bool
115      */
116     private $_port = false;
117
118     /**
119      * @var  string
120      */
121     private $_path = '';
122
123     /**
124      * @var  string|bool
125      */
126     private $_query = false;
127
128     /**
129      * @var  string|bool
130      */
131     private $_fragment = false;
132
133     /**
134      * Constructor.
135      *
136      * @param string $url     an absolute or relative URL
137      * @param array  $options an array of OPTION_xxx constants
138      *
139      * @return $this
140      * @uses   self::parseUrl()
141      */
142     public function __construct($url, array $options = array())
143     {
144         foreach ($options as $optionName => $value) {
145             if (array_key_exists($optionName, $this->_options)) {
146                 $this->_options[$optionName] = $value;
147             }
148         }
149
150         $this->parseUrl($url);
151     }
152
153     /**
154      * Magic Setter.
155      *
156      * This method will magically set the value of a private variable ($var)
157      * with the value passed as the args
158      *
159      * @param  string $var      The private variable to set.
160      * @param  mixed  $arg      An argument of any type.
161      * @return void
162      */
163     public function __set($var, $arg)
164     {
165         $method = 'set' . $var;
166         if (method_exists($this, $method)) {
167             $this->$method($arg);
168         }
169     }
170
171     /**
172      * Magic Getter.
173      *
174      * This is the magic get method to retrieve the private variable
175      * that was set by either __set() or it's setter...
176      *
177      * @param  string $var         The property name to retrieve.
178      * @return mixed  $this->$var  Either a boolean false if the
179      *                             property is not set or the value
180      *                             of the private property.
181      */
182     public function __get($var)
183     {
184         $method = 'get' . $var;
185         if (method_exists($this, $method)) {
186             return $this->$method();
187         }
188
189         return false;
190     }
191
192     /**
193      * Returns the scheme, e.g. "http" or "urn", or false if there is no
194      * scheme specified, i.e. if this is a relative URL.
195      *
196      * @return  string|bool
197      */
198     public function getScheme()
199     {
200         return $this->_scheme;
201     }
202
203     /**
204      * Sets the scheme, e.g. "http" or "urn". Specify false if there is no
205      * scheme specified, i.e. if this is a relative URL.
206      *
207      * @param string|bool $scheme e.g. "http" or "urn", or false if there is no
208      *                            scheme specified, i.e. if this is a relative
209      *                            URL
210      *
211      * @return $this
212      * @see    getScheme()
213      */
214     public function setScheme($scheme)
215     {
216         $this->_scheme = $scheme;
217         return $this;
218     }
219
220     /**
221      * Returns the user part of the userinfo part (the part preceding the first
222      *  ":"), or false if there is no userinfo part.
223      *
224      * @return  string|bool
225      */
226     public function getUser()
227     {
228         return $this->_userinfo !== false
229             ? preg_replace('@:.*$@', '', $this->_userinfo)
230             : false;
231     }
232
233     /**
234      * Returns the password part of the userinfo part (the part after the first
235      *  ":"), or false if there is no userinfo part (i.e. the URL does not
236      * contain "@" in front of the hostname) or the userinfo part does not
237      * contain ":".
238      *
239      * @return  string|bool
240      */
241     public function getPassword()
242     {
243         return $this->_userinfo !== false
244             ? substr(strstr($this->_userinfo, ':'), 1)
245             : false;
246     }
247
248     /**
249      * Returns the userinfo part, or false if there is none, i.e. if the
250      * authority part does not contain "@".
251      *
252      * @return  string|bool
253      */
254     public function getUserinfo()
255     {
256         return $this->_userinfo;
257     }
258
259     /**
260      * Sets the userinfo part. If two arguments are passed, they are combined
261      * in the userinfo part as username ":" password.
262      *
263      * @param string|bool $userinfo userinfo or username
264      * @param string|bool $password optional password, or false
265      *
266      * @return $this
267      */
268     public function setUserinfo($userinfo, $password = false)
269     {
270         $this->_userinfo = $userinfo;
271         if ($password !== false) {
272             $this->_userinfo .= ':' . $password;
273         }
274         return $this;
275     }
276
277     /**
278      * Returns the host part, or false if there is no authority part, e.g.
279      * relative URLs.
280      *
281      * @return  string|bool a hostname, an IP address, or false
282      */
283     public function getHost()
284     {
285         return $this->_host;
286     }
287
288     /**
289      * Sets the host part. Specify false if there is no authority part, e.g.
290      * relative URLs.
291      *
292      * @param string|bool $host a hostname, an IP address, or false
293      *
294      * @return $this
295      */
296     public function setHost($host)
297     {
298         $this->_host = $host;
299         return $this;
300     }
301
302     /**
303      * Returns the port number, or false if there is no port number specified,
304      * i.e. if the default port is to be used.
305      *
306      * @return  string|bool
307      */
308     public function getPort()
309     {
310         return $this->_port;
311     }
312
313     /**
314      * Sets the port number. Specify false if there is no port number specified,
315      * i.e. if the default port is to be used.
316      *
317      * @param string|bool $port a port number, or false
318      *
319      * @return $this
320      */
321     public function setPort($port)
322     {
323         $this->_port = $port;
324         return $this;
325     }
326
327     /**
328      * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
329      * false if there is no authority.
330      *
331      * @return string|bool
332      */
333     public function getAuthority()
334     {
335         if (!$this->_host) {
336             return false;
337         }
338
339         $authority = '';
340
341         if ($this->_userinfo !== false) {
342             $authority .= $this->_userinfo . '@';
343         }
344
345         $authority .= $this->_host;
346
347         if ($this->_port !== false) {
348             $authority .= ':' . $this->_port;
349         }
350
351         return $authority;
352     }
353
354     /**
355      * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify
356      * false if there is no authority.
357      *
358      * @param string|false $authority a hostname or an IP addresse, possibly
359      *                                with userinfo prefixed and port number
360      *                                appended, e.g. "foo:bar@example.org:81".
361      *
362      * @return $this
363      */
364     public function setAuthority($authority)
365     {
366         $this->_userinfo = false;
367         $this->_host     = false;
368         $this->_port     = false;
369         if (preg_match('@^(([^\@]*)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
370             if ($reg[1]) {
371                 $this->_userinfo = $reg[2];
372             }
373
374             $this->_host = $reg[3];
375             if (isset($reg[5])) {
376                 $this->_port = $reg[5];
377             }
378         }
379         return $this;
380     }
381
382     /**
383      * Returns the path part (possibly an empty string).
384      *
385      * @return string
386      */
387     public function getPath()
388     {
389         return $this->_path;
390     }
391
392     /**
393      * Sets the path part (possibly an empty string).
394      *
395      * @param string $path a path
396      *
397      * @return $this
398      */
399     public function setPath($path)
400     {
401         $this->_path = $path;
402         return $this;
403     }
404
405     /**
406      * Returns the query string (excluding the leading "?"), or false if "?"
407      * is not present in the URL.
408      *
409      * @return  string|bool
410      * @see     self::getQueryVariables()
411      */
412     public function getQuery()
413     {
414         return $this->_query;
415     }
416
417     /**
418      * Sets the query string (excluding the leading "?"). Specify false if "?"
419      * is not present in the URL.
420      *
421      * @param string|bool $query a query string, e.g. "foo=1&bar=2"
422      *
423      * @return $this
424      * @see    self::setQueryVariables()
425      */
426     public function setQuery($query)
427     {
428         $this->_query = $query;
429         return $this;
430     }
431
432     /**
433      * Returns the fragment name, or false if "#" is not present in the URL.
434      *
435      * @return  string|bool
436      */
437     public function getFragment()
438     {
439         return $this->_fragment;
440     }
441
442     /**
443      * Sets the fragment name. Specify false if "#" is not present in the URL.
444      *
445      * @param string|bool $fragment a fragment excluding the leading "#", or
446      *                              false
447      *
448      * @return $this
449      */
450     public function setFragment($fragment)
451     {
452         $this->_fragment = $fragment;
453         return $this;
454     }
455
456     /**
457      * Returns the query string like an array as the variables would appear in
458      * $_GET in a PHP script. If the URL does not contain a "?", an empty array
459      * is returned.
460      *
461      * @return  array
462      */
463     public function getQueryVariables()
464     {
465         $pattern = '/[' .
466                    preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
467                    ']/';
468         $parts   = preg_split($pattern, $this->_query, -1, PREG_SPLIT_NO_EMPTY);
469         $return  = array();
470
471         foreach ($parts as $part) {
472             if (strpos($part, '=') !== false) {
473                 list($key, $value) = explode('=', $part, 2);
474             } else {
475                 $key   = $part;
476                 $value = null;
477             }
478
479             if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
480                 $key = rawurldecode($key);
481             }
482             $value = rawurldecode($value);
483
484             if ($this->getOption(self::OPTION_USE_BRACKETS) &&
485                 preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
486
487                 $key = $matches[1];
488                 $idx = $matches[2];
489
490                 // Ensure is an array
491                 if (empty($return[$key]) || !is_array($return[$key])) {
492                     $return[$key] = array();
493                 }
494
495                 // Add data
496                 if ($idx === '') {
497                     $return[$key][] = $value;
498                 } else {
499                     $return[$key][$idx] = $value;
500                 }
501             } elseif (!$this->getOption(self::OPTION_USE_BRACKETS)
502                       && !empty($return[$key])
503             ) {
504                 $return[$key]   = (array) $return[$key];
505                 $return[$key][] = $value;
506             } else {
507                 $return[$key] = $value;
508             }
509         }
510
511         return $return;
512     }
513
514     /**
515      * Sets the query string to the specified variable in the query string.
516      *
517      * @param array $array (name => value) array
518      *
519      * @return $this
520      */
521     public function setQueryVariables(array $array)
522     {
523         if (!$array) {
524             $this->_query = false;
525         } else {
526             $this->_query = $this->buildQuery(
527                 $array,
528                 $this->getOption(self::OPTION_SEPARATOR_OUTPUT)
529             );
530         }
531         return $this;
532     }
533
534     /**
535      * Sets the specified variable in the query string.
536      *
537      * @param string $name  variable name
538      * @param mixed  $value variable value
539      *
540      * @return $this
541      */
542     public function setQueryVariable($name, $value)
543     {
544         $array = $this->getQueryVariables();
545         $array[$name] = $value;
546         $this->setQueryVariables($array);
547         return $this;
548     }
549
550     /**
551      * Removes the specifed variable from the query string.
552      *
553      * @param string $name a query string variable, e.g. "foo" in "?foo=1"
554      *
555      * @return void
556      */
557     public function unsetQueryVariable($name)
558     {
559         $array = $this->getQueryVariables();
560         unset($array[$name]);
561         $this->setQueryVariables($array);
562     }
563
564     /**
565      * Returns a string representation of this URL.
566      *
567      * @return  string
568      */
569     public function getURL()
570     {
571         // See RFC 3986, section 5.3
572         $url = "";
573
574         if ($this->_scheme !== false) {
575             $url .= $this->_scheme . ':';
576         }
577
578         $authority = $this->getAuthority();
579         if ($authority !== false) {
580             $url .= '//' . $authority;
581         }
582         $url .= $this->_path;
583
584         if ($this->_query !== false) {
585             $url .= '?' . $this->_query;
586         }
587
588         if ($this->_fragment !== false) {
589             $url .= '#' . $this->_fragment;
590         }
591     
592         return $url;
593     }
594
595     /**
596      * Returns a string representation of this URL.
597      *
598      * @return  string
599      * @see toString()
600      */
601     public function __toString()
602     {
603         return $this->getURL();
604     }
605
606     /** 
607      * Returns a normalized string representation of this URL. This is useful
608      * for comparison of URLs.
609      *
610      * @return  string
611      */
612     public function getNormalizedURL()
613     {
614         $url = clone $this;
615         $url->normalize();
616         return $url->getUrl();
617     }
618
619     /** 
620      * Returns a normalized Net_URL2 instance.
621      *
622      * @return  Net_URL2
623      */
624     public function normalize()
625     {
626         // See RFC 3886, section 6
627
628         // Schemes are case-insensitive
629         if ($this->_scheme) {
630             $this->_scheme = strtolower($this->_scheme);
631         }
632
633         // Hostnames are case-insensitive
634         if ($this->_host) {
635             $this->_host = strtolower($this->_host);
636         }
637
638         // Remove default port number for known schemes (RFC 3986, section 6.2.3)
639         if ($this->_port &&
640             $this->_scheme &&
641             $this->_port == getservbyname($this->_scheme, 'tcp')) {
642
643             $this->_port = false;
644         }
645
646         // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
647         foreach (array('_userinfo', '_host', '_path') as $part) {
648             if ($this->$part) {
649                 $this->$part = preg_replace('/%[0-9a-f]{2}/ie',
650                                             'strtoupper("\0")',
651                                             $this->$part);
652             }
653         }
654
655         // Path segment normalization (RFC 3986, section 6.2.2.3)
656         $this->_path = self::removeDotSegments($this->_path);
657
658         // Scheme based normalization (RFC 3986, section 6.2.3)
659         if ($this->_host && !$this->_path) {
660             $this->_path = '/';
661         }
662     }
663
664     /**
665      * Returns whether this instance represents an absolute URL.
666      *
667      * @return  bool
668      */
669     public function isAbsolute()
670     {
671         return (bool) $this->_scheme;
672     }
673
674     /**
675      * Returns an Net_URL2 instance representing an absolute URL relative to
676      * this URL.
677      *
678      * @param Net_URL2|string $reference relative URL
679      *
680      * @return Net_URL2
681      */
682     public function resolve($reference)
683     {
684         if (!$reference instanceof Net_URL2) {
685             $reference = new self($reference);
686         }
687         if (!$this->isAbsolute()) {
688             throw new Exception('Base-URL must be absolute');
689         }
690
691         // A non-strict parser may ignore a scheme in the reference if it is
692         // identical to the base URI's scheme.
693         if (!$this->getOption(self::OPTION_STRICT) && $reference->_scheme == $this->_scheme) {
694             $reference->_scheme = false;
695         }
696
697         $target = new self('');
698         if ($reference->_scheme !== false) {
699             $target->_scheme = $reference->_scheme;
700             $target->setAuthority($reference->getAuthority());
701             $target->_path  = self::removeDotSegments($reference->_path);
702             $target->_query = $reference->_query;
703         } else {
704             $authority = $reference->getAuthority();
705             if ($authority !== false) {
706                 $target->setAuthority($authority);
707                 $target->_path  = self::removeDotSegments($reference->_path);
708                 $target->_query = $reference->_query;
709             } else {
710                 if ($reference->_path == '') {
711                     $target->_path = $this->_path;
712                     if ($reference->_query !== false) {
713                         $target->_query = $reference->_query;
714                     } else {
715                         $target->_query = $this->_query;
716                     }
717                 } else {
718                     if (substr($reference->_path, 0, 1) == '/') {
719                         $target->_path = self::removeDotSegments($reference->_path);
720                     } else {
721                         // Merge paths (RFC 3986, section 5.2.3)
722                         if ($this->_host !== false && $this->_path == '') {
723                             $target->_path = '/' . $this->_path;
724                         } else {
725                             $i = strrpos($this->_path, '/');
726                             if ($i !== false) {
727                                 $target->_path = substr($this->_path, 0, $i + 1);
728                             }
729                             $target->_path .= $reference->_path;
730                         }
731                         $target->_path = self::removeDotSegments($target->_path);
732                     }
733                     $target->_query = $reference->_query;
734                 }
735                 $target->setAuthority($this->getAuthority());
736             }
737             $target->_scheme = $this->_scheme;
738         }
739
740         $target->_fragment = $reference->_fragment;
741
742         return $target;
743     }
744
745     /**
746      * Removes dots as described in RFC 3986, section 5.2.4, e.g.
747      * "/foo/../bar/baz" => "/bar/baz"
748      *
749      * @param string $path a path
750      *
751      * @return string a path
752      */
753     public static function removeDotSegments($path)
754     {
755         $output = '';
756
757         // Make sure not to be trapped in an infinite loop due to a bug in this
758         // method
759         $j = 0;
760         while ($path && $j++ < 100) {
761             if (substr($path, 0, 2) == './') {
762                 // Step 2.A
763                 $path = substr($path, 2);
764             } elseif (substr($path, 0, 3) == '../') {
765                 // Step 2.A
766                 $path = substr($path, 3);
767             } elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
768                 // Step 2.B
769                 $path = '/' . substr($path, 3);
770             } elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
771                 // Step 2.C
772                 $path   = '/' . substr($path, 4);
773                 $i      = strrpos($output, '/');
774                 $output = $i === false ? '' : substr($output, 0, $i);
775             } elseif ($path == '.' || $path == '..') {
776                 // Step 2.D
777                 $path = '';
778             } else {
779                 // Step 2.E
780                 $i = strpos($path, '/');
781                 if ($i === 0) {
782                     $i = strpos($path, '/', 1);
783                 }
784                 if ($i === false) {
785                     $i = strlen($path);
786                 }
787                 $output .= substr($path, 0, $i);
788                 $path = substr($path, $i);
789             }
790         }
791
792         return $output;
793     }
794
795     /**
796      * Percent-encodes all non-alphanumeric characters except these: _ . - ~
797      * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP
798      * 5.2.x and earlier.
799      *
800      * @param  $raw the string to encode
801      * @return string
802      */
803     public static function urlencode($string)
804     {
805         $encoded = rawurlencode($string);
806
807         // This is only necessary in PHP < 5.3.
808         $encoded = str_replace('%7E', '~', $encoded);
809         return $encoded;
810     }
811
812     /**
813      * Returns a Net_URL2 instance representing the canonical URL of the
814      * currently executing PHP script.
815      *
816      * @return  string
817      */
818     public static function getCanonical()
819     {
820         if (!isset($_SERVER['REQUEST_METHOD'])) {
821             // ALERT - no current URL
822             throw new Exception('Script was not called through a webserver');
823         }
824
825         // Begin with a relative URL
826         $url = new self($_SERVER['PHP_SELF']);
827         $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
828         $url->_host   = $_SERVER['SERVER_NAME'];
829         $port = $_SERVER['SERVER_PORT'];
830         if ($url->_scheme == 'http' && $port != 80 ||
831             $url->_scheme == 'https' && $port != 443) {
832
833             $url->_port = $port;
834         }
835         return $url;
836     }
837
838     /**
839      * Returns the URL used to retrieve the current request.
840      *
841      * @return  string
842      */
843     public static function getRequestedURL()
844     {
845         return self::getRequested()->getUrl();
846     }
847
848     /**
849      * Returns a Net_URL2 instance representing the URL used to retrieve the
850      * current request.
851      *
852      * @return  Net_URL2
853      */
854     public static function getRequested()
855     {
856         if (!isset($_SERVER['REQUEST_METHOD'])) {
857             // ALERT - no current URL
858             throw new Exception('Script was not called through a webserver');
859         }
860
861         // Begin with a relative URL
862         $url = new self($_SERVER['REQUEST_URI']);
863         $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
864         // Set host and possibly port
865         $url->setAuthority($_SERVER['HTTP_HOST']);
866         return $url;
867     }
868
869     /**
870      * Returns the value of the specified option.
871      *
872      * @param string $optionName The name of the option to retrieve
873      *
874      * @return  mixed
875      */
876     public function getOption($optionName)
877     {
878         return isset($this->_options[$optionName])
879             ? $this->_options[$optionName] : false;
880     }
881
882     /**
883      * A simple version of http_build_query in userland. The encoded string is
884      * percentage encoded according to RFC 3986.
885      *
886      * @param array  $data      An array, which has to be converted into
887      *                          QUERY_STRING. Anything is possible.
888      * @param string $seperator See {@link self::OPTION_SEPARATOR_OUTPUT}
889      * @param string $key       For stacked values (arrays in an array).
890      *
891      * @return string
892      */
893     protected function buildQuery(array $data, $separator, $key = null)
894     {
895         $query = array();
896         foreach ($data as $name => $value) {
897             if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) {
898                 $name = rawurlencode($name);
899             }
900             if ($key !== null) {
901                 if ($this->getOption(self::OPTION_USE_BRACKETS) === true) {
902                     $name = $key . '[' . $name . ']';
903                 } else {
904                     $name = $key;
905                 }
906             }
907             if (is_array($value)) {
908                 $query[] = $this->buildQuery($value, $separator, $name);
909             } else {
910                 $query[] = $name . '=' . rawurlencode($value);
911             }
912         }
913         return implode($separator, $query);
914     }
915
916     /**
917      * This method uses a funky regex to parse the url into the designated parts.
918      *
919      * @param string $url
920      *
921      * @return void
922      * @uses   self::$_scheme, self::setAuthority(), self::$_path, self::$_query,
923      *         self::$_fragment
924      * @see    self::__construct()
925      */
926     protected function parseUrl($url)
927     {
928         // The regular expression is copied verbatim from RFC 3986, appendix B.
929         // The expression does not validate the URL but matches any string.
930         preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!',
931                    $url,
932                    $matches);
933
934         // "path" is always present (possibly as an empty string); the rest
935         // are optional.
936         $this->_scheme   = !empty($matches[1]) ? $matches[2] : false;
937         $this->setAuthority(!empty($matches[3]) ? $matches[4] : false);
938         $this->_path     = $matches[5];
939         $this->_query    = !empty($matches[6]) ? $matches[7] : false;
940         $this->_fragment = !empty($matches[8]) ? $matches[9] : false;
941     }
942 }