]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Net/URL2.php
Merge branch '0.7.x' into 0.8.x
[quix0rs-gnu-social.git] / extlib / Net / URL2.php
1 <?php
2 // +-----------------------------------------------------------------------+
3 // | Copyright (c) 2007-2008, Christian Schmidt, Peytz & Co. A/S           |
4 // | All rights reserved.                                                  |
5 // |                                                                       |
6 // | Redistribution and use in source and binary forms, with or without    |
7 // | modification, are permitted provided that the following conditions    |
8 // | are met:                                                              |
9 // |                                                                       |
10 // | o Redistributions of source code must retain the above copyright      |
11 // |   notice, this list of conditions and the following disclaimer.       |
12 // | o Redistributions in binary form must reproduce the above copyright   |
13 // |   notice, this list of conditions and the following disclaimer in the |
14 // |   documentation and/or other materials provided with the distribution.|
15 // | o The names of the authors may not be used to endorse or promote      |
16 // |   products derived from this software without specific prior written  |
17 // |   permission.                                                         |
18 // |                                                                       |
19 // | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
20 // | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
21 // | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
22 // | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
23 // | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
24 // | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
25 // | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
26 // | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
27 // | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
28 // | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
29 // | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
30 // |                                                                       |
31 // +-----------------------------------------------------------------------+
32 // | Author: Christian Schmidt <schmidt at php dot net>                    |
33 // +-----------------------------------------------------------------------+
34 //
35 // $Id: URL2.php,v 1.10 2008/04/26 21:57:08 schmidt Exp $
36 //
37 // Net_URL2 Class (PHP5 Only)
38
39 // This code is released under the BSD License - http://www.opensource.org/licenses/bsd-license.php
40 /**
41  * @license BSD License
42  */
43 class Net_URL2
44 {
45     /**
46      * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default
47      * is true.
48      */
49     const OPTION_STRICT           = 'strict';
50
51     /**
52      * Represent arrays in query using PHP's [] notation. Default is true.
53      */
54     const OPTION_USE_BRACKETS     = 'use_brackets';
55
56     /**
57      * URL-encode query variable keys. Default is true.
58      */
59     const OPTION_ENCODE_KEYS      = 'encode_keys';
60
61     /**
62      * Query variable separators when parsing the query string. Every character
63      * is considered a separator. Default is specified by the
64      * arg_separator.input php.ini setting (this defaults to "&").
65      */
66     const OPTION_SEPARATOR_INPUT  = 'input_separator';
67
68     /**
69      * Query variable separator used when generating the query string. Default
70      * is specified by the arg_separator.output php.ini setting (this defaults
71      * to "&").
72      */
73     const OPTION_SEPARATOR_OUTPUT = 'output_separator';
74
75     /**
76      * Default options corresponds to how PHP handles $_GET.
77      */
78     private $options = array(
79         self::OPTION_STRICT           => true,
80         self::OPTION_USE_BRACKETS     => true,
81         self::OPTION_ENCODE_KEYS      => true,
82         self::OPTION_SEPARATOR_INPUT  => 'x&',
83         self::OPTION_SEPARATOR_OUTPUT => 'x&',
84         );
85
86     /**
87      * @var  string|bool
88      */
89     private $scheme = false;
90
91     /**
92      * @var  string|bool
93      */
94     private $userinfo = false;
95
96     /**
97      * @var  string|bool
98      */
99     private $host = false;
100
101     /**
102      * @var  int|bool
103      */
104     private $port = false;
105
106     /**
107      * @var  string
108      */
109     private $path = '';
110
111     /**
112      * @var  string|bool
113      */
114     private $query = false;
115
116     /**
117      * @var  string|bool
118      */
119     private $fragment = false;
120
121     /**
122      * @param string $url     an absolute or relative URL
123      * @param array  $options
124      */
125     public function __construct($url, $options = null)
126     {
127         $this->setOption(self::OPTION_SEPARATOR_INPUT,
128                          ini_get('arg_separator.input'));
129         $this->setOption(self::OPTION_SEPARATOR_OUTPUT,
130                          ini_get('arg_separator.output'));
131         if (is_array($options)) {
132             foreach ($options as $optionName => $value) {
133                 $this->setOption($optionName);
134             }
135         }
136
137         if (preg_match('@^([a-z][a-z0-9.+-]*):@i', $url, $reg)) {
138             $this->scheme = $reg[1];
139             $url = substr($url, strlen($reg[0]));
140         }
141
142         if (preg_match('@^//([^/#?]+)@', $url, $reg)) {
143             $this->setAuthority($reg[1]);
144             $url = substr($url, strlen($reg[0]));
145         }
146
147         $i = strcspn($url, '?#');
148         $this->path = substr($url, 0, $i);
149         $url = substr($url, $i);
150
151         if (preg_match('@^\?([^#]*)@', $url, $reg)) {
152             $this->query = $reg[1];
153             $url = substr($url, strlen($reg[0]));
154         }
155
156         if ($url) {
157             $this->fragment = substr($url, 1);
158         }
159     }
160
161     /**
162      * Returns the scheme, e.g. "http" or "urn", or false if there is no
163      * scheme specified, i.e. if this is a relative URL.
164      *
165      * @return  string|bool
166      */
167     public function getScheme()
168     {
169         return $this->scheme;
170     }
171
172     /**
173      * @param string|bool $scheme
174      *
175      * @return void
176      * @see    getScheme()
177      */
178     public function setScheme($scheme)
179     {
180         $this->scheme = $scheme;
181     }
182
183     /**
184      * Returns the user part of the userinfo part (the part preceding the first
185      *  ":"), or false if there is no userinfo part.
186      *
187      * @return  string|bool
188      */
189     public function getUser()
190     {
191         return $this->userinfo !== false ? preg_replace('@:.*$@', '', $this->userinfo) : false;
192     }
193
194     /**
195      * Returns the password part of the userinfo part (the part after the first
196      *  ":"), or false if there is no userinfo part (i.e. the URL does not
197      * contain "@" in front of the hostname) or the userinfo part does not
198      * contain ":".
199      *
200      * @return  string|bool
201      */
202     public function getPassword()
203     {
204         return $this->userinfo !== false ? substr(strstr($this->userinfo, ':'), 1) : false;
205     }
206
207     /**
208      * Returns the userinfo part, or false if there is none, i.e. if the
209      * authority part does not contain "@".
210      *
211      * @return  string|bool
212      */
213     public function getUserinfo()
214     {
215         return $this->userinfo;
216     }
217
218     /**
219      * Sets the userinfo part. If two arguments are passed, they are combined
220      * in the userinfo part as username ":" password.
221      *
222      * @param string|bool $userinfo userinfo or username
223      * @param string|bool $password
224      *
225      * @return void
226      */
227     public function setUserinfo($userinfo, $password = false)
228     {
229         $this->userinfo = $userinfo;
230         if ($password !== false) {
231             $this->userinfo .= ':' . $password;
232         }
233     }
234
235     /**
236      * Returns the host part, or false if there is no authority part, e.g.
237      * relative URLs.
238      *
239      * @return  string|bool
240      */
241     public function getHost()
242     {
243         return $this->host;
244     }
245
246     /**
247      * @param string|bool $host
248      *
249      * @return void
250      */
251     public function setHost($host)
252     {
253         $this->host = $host;
254     }
255
256     /**
257      * Returns the port number, or false if there is no port number specified,
258      * i.e. if the default port is to be used.
259      *
260      * @return  int|bool
261      */
262     public function getPort()
263     {
264         return $this->port;
265     }
266
267     /**
268      * @param int|bool $port
269      *
270      * @return void
271      */
272     public function setPort($port)
273     {
274         $this->port = intval($port);
275     }
276
277     /**
278      * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
279      * false if there is no authority none.
280      *
281      * @return string|bool
282      */
283     public function getAuthority()
284     {
285         if (!$this->host) {
286             return false;
287         }
288
289         $authority = '';
290
291         if ($this->userinfo !== false) {
292             $authority .= $this->userinfo . '@';
293         }
294
295         $authority .= $this->host;
296
297         if ($this->port !== false) {
298             $authority .= ':' . $this->port;
299         }
300
301         return $authority;
302     }
303
304     /**
305      * @param string|false $authority
306      *
307      * @return void
308      */
309     public function setAuthority($authority)
310     {
311         $this->user = false;
312         $this->pass = false;
313         $this->host = false;
314         $this->port = false;
315         if (preg_match('@^(([^\@]+)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
316             if ($reg[1]) {
317                 $this->userinfo = $reg[2];
318             }
319
320             $this->host = $reg[3];
321             if (isset($reg[5])) {
322                 $this->port = intval($reg[5]);
323             }
324         }
325     }
326
327     /**
328      * Returns the path part (possibly an empty string).
329      *
330      * @return string
331      */
332     public function getPath()
333     {
334         return $this->path;
335     }
336
337     /**
338      * @param string $path
339      *
340      * @return void
341      */
342     public function setPath($path)
343     {
344         $this->path = $path;
345     }
346
347     /**
348      * Returns the query string (excluding the leading "?"), or false if "?"
349      * isn't present in the URL.
350      *
351      * @return  string|bool
352      * @see     self::getQueryVariables()
353      */
354     public function getQuery()
355     {
356         return $this->query;
357     }
358
359     /**
360      * @param string|bool $query
361      *
362      * @return void
363      * @see   self::setQueryVariables()
364      */
365     public function setQuery($query)
366     {
367         $this->query = $query;
368     }
369
370     /**
371      * Returns the fragment name, or false if "#" isn't present in the URL.
372      *
373      * @return  string|bool
374      */
375     public function getFragment()
376     {
377         return $this->fragment;
378     }
379
380     /**
381      * @param string|bool $fragment
382      *
383      * @return void
384      */
385     public function setFragment($fragment)
386     {
387         $this->fragment = $fragment;
388     }
389
390     /**
391      * Returns the query string like an array as the variables would appear in
392      * $_GET in a PHP script.
393      *
394      * @return  array
395      */
396     public function getQueryVariables()
397     {
398         $pattern = '/[' .
399                    preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
400                    ']/';
401         $parts   = preg_split($pattern, $this->query, -1, PREG_SPLIT_NO_EMPTY);
402         $return  = array();
403
404         foreach ($parts as $part) {
405             if (strpos($part, '=') !== false) {
406                 list($key, $value) = explode('=', $part, 2);
407             } else {
408                 $key   = $part;
409                 $value = null;
410             }
411
412             if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
413                 $key = rawurldecode($key);
414             }
415             $value = rawurldecode($value);
416
417             if ($this->getOption(self::OPTION_USE_BRACKETS) &&
418                 preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
419
420                 $key = $matches[1];
421                 $idx = $matches[2];
422
423                 // Ensure is an array
424                 if (empty($return[$key]) || !is_array($return[$key])) {
425                     $return[$key] = array();
426                 }
427
428                 // Add data
429                 if ($idx === '') {
430                     $return[$key][] = $value;
431                 } else {
432                     $return[$key][$idx] = $value;
433                 }
434             } elseif (!$this->getOption(self::OPTION_USE_BRACKETS)
435                       && !empty($return[$key])
436             ) {
437                 $return[$key]   = (array) $return[$key];
438                 $return[$key][] = $value;
439             } else {
440                 $return[$key] = $value;
441             }
442         }
443
444         return $return;
445     }
446
447     /**
448      * @param array $array (name => value) array
449      *
450      * @return void
451      */
452     public function setQueryVariables(array $array)
453     {
454         if (!$array) {
455             $this->query = false;
456         } else {
457             foreach ($array as $name => $value) {
458                 if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
459                     $name = rawurlencode($name);
460                 }
461
462                 if (is_array($value)) {
463                     foreach ($value as $k => $v) {
464                         $parts[] = $this->getOption(self::OPTION_USE_BRACKETS)
465                             ? sprintf('%s[%s]=%s', $name, $k, $v)
466                             : ($name . '=' . $v);
467                     }
468                 } elseif (!is_null($value)) {
469                     $parts[] = $name . '=' . $value;
470                 } else {
471                     $parts[] = $name;
472                 }
473             }
474             $this->query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT),
475                                    $parts);
476         }
477     }
478
479     /**
480      * @param string $name
481      * @param mixed  $value
482      *
483      * @return  array
484      */
485     public function setQueryVariable($name, $value)
486     {
487         $array = $this->getQueryVariables();
488         $array[$name] = $value;
489         $this->setQueryVariables($array);
490     }
491
492     /**
493      * @param string $name
494      *
495      * @return void
496      */
497     public function unsetQueryVariable($name)
498     {
499         $array = $this->getQueryVariables();
500         unset($array[$name]);
501         $this->setQueryVariables($array);
502     }
503
504     /**
505      * Returns a string representation of this URL.
506      *
507      * @return  string
508      */
509     public function getURL()
510     {
511         // See RFC 3986, section 5.3
512         $url = "";
513
514         if ($this->scheme !== false) {
515             $url .= $this->scheme . ':';
516         }
517
518         $authority = $this->getAuthority();
519         if ($authority !== false) {
520             $url .= '//' . $authority;
521         }
522         $url .= $this->path;
523
524         if ($this->query !== false) {
525             $url .= '?' . $this->query;
526         }
527
528         if ($this->fragment !== false) {
529             $url .= '#' . $this->fragment;
530         }
531     
532         return $url;
533     }
534
535     /** 
536      * Returns a normalized string representation of this URL. This is useful
537      * for comparison of URLs.
538      *
539      * @return  string
540      */
541     public function getNormalizedURL()
542     {
543         $url = clone $this;
544         $url->normalize();
545         return $url->getUrl();
546     }
547
548     /** 
549      * Returns a normalized Net_URL2 instance.
550      *
551      * @return  Net_URL2
552      */
553     public function normalize()
554     {
555         // See RFC 3886, section 6
556
557         // Schemes are case-insensitive
558         if ($this->scheme) {
559             $this->scheme = strtolower($this->scheme);
560         }
561
562         // Hostnames are case-insensitive
563         if ($this->host) {
564             $this->host = strtolower($this->host);
565         }
566
567         // Remove default port number for known schemes (RFC 3986, section 6.2.3)
568         if ($this->port &&
569             $this->scheme &&
570             $this->port == getservbyname($this->scheme, 'tcp')) {
571
572             $this->port = false;
573         }
574
575         // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
576         foreach (array('userinfo', 'host', 'path') as $part) {
577             if ($this->$part) {
578                 $this->$part  = preg_replace('/%[0-9a-f]{2}/ie', 'strtoupper("\0")', $this->$part);
579             }
580         }
581
582         // Path segment normalization (RFC 3986, section 6.2.2.3)
583         $this->path = self::removeDotSegments($this->path);
584
585         // Scheme based normalization (RFC 3986, section 6.2.3)
586         if ($this->host && !$this->path) {
587             $this->path = '/';
588         }
589     }
590
591     /**
592      * Returns whether this instance represents an absolute URL.
593      *
594      * @return  bool
595      */
596     public function isAbsolute()
597     {
598         return (bool) $this->scheme;
599     }
600
601     /**
602      * Returns an Net_URL2 instance representing an absolute URL relative to
603      * this URL.
604      *
605      * @param Net_URL2|string $reference relative URL
606      *
607      * @return Net_URL2
608      */
609     public function resolve($reference)
610     {
611         if (is_string($reference)) {
612             $reference = new self($reference);
613         }
614         if (!$this->isAbsolute()) {
615             throw new Exception('Base-URL must be absolute');
616         }
617
618         // A non-strict parser may ignore a scheme in the reference if it is
619         // identical to the base URI's scheme.
620         if (!$this->getOption(self::OPTION_STRICT) && $reference->scheme == $this->scheme) {
621             $reference->scheme = false;
622         }
623
624         $target = new self('');
625         if ($reference->scheme !== false) {
626             $target->scheme = $reference->scheme;
627             $target->setAuthority($reference->getAuthority());
628             $target->path  = self::removeDotSegments($reference->path);
629             $target->query = $reference->query;
630         } else {
631             $authority = $reference->getAuthority();
632             if ($authority !== false) {
633                 $target->setAuthority($authority);
634                 $target->path  = self::removeDotSegments($reference->path);
635                 $target->query = $reference->query;
636             } else {
637                 if ($reference->path == '') {
638                     $target->path = $this->path;
639                     if ($reference->query !== false) {
640                         $target->query = $reference->query;
641                     } else {
642                         $target->query = $this->query;
643                     }
644                 } else {
645                     if (substr($reference->path, 0, 1) == '/') {
646                         $target->path = self::removeDotSegments($reference->path);
647                     } else {
648                         // Merge paths (RFC 3986, section 5.2.3)
649                         if ($this->host !== false && $this->path == '') {
650                             $target->path = '/' . $this->path;
651                         } else {
652                             $i = strrpos($this->path, '/');
653                             if ($i !== false) {
654                                 $target->path = substr($this->path, 0, $i + 1);
655                             }
656                             $target->path .= $reference->path;
657                         }
658                         $target->path = self::removeDotSegments($target->path);
659                     }
660                     $target->query = $reference->query;
661                 }
662                 $target->setAuthority($this->getAuthority());
663             }
664             $target->scheme = $this->scheme;
665         }
666
667         $target->fragment = $reference->fragment;
668
669         return $target;
670     }
671
672     /**
673      * Removes dots as described in RFC 3986, section 5.2.4, e.g.
674      * "/foo/../bar/baz" => "/bar/baz"
675      *
676      * @param string $path a path
677      *
678      * @return string a path
679      */
680     private static function removeDotSegments($path)
681     {
682         $output = '';
683
684         // Make sure not to be trapped in an infinite loop due to a bug in this
685         // method
686         $j = 0; 
687         while ($path && $j++ < 100) {
688             // Step A
689             if (substr($path, 0, 2) == './') {
690                 $path = substr($path, 2);
691             } elseif (substr($path, 0, 3) == '../') {
692                 $path = substr($path, 3);
693
694             // Step B
695             } elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
696                 $path = '/' . substr($path, 3);
697
698             // Step C
699             } elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
700                 $path = '/' . substr($path, 4);
701                 $i = strrpos($output, '/');
702                 $output = $i === false ? '' : substr($output, 0, $i);
703
704             // Step D
705             } elseif ($path == '.' || $path == '..') {
706                 $path = '';
707
708             // Step E
709             } else {
710                 $i = strpos($path, '/');
711                 if ($i === 0) {
712                     $i = strpos($path, '/', 1);
713                 }
714                 if ($i === false) {
715                     $i = strlen($path);
716                 }
717                 $output .= substr($path, 0, $i);
718                 $path = substr($path, $i);
719             }
720         }
721
722         return $output;
723     }
724
725     /**
726      * Returns a Net_URL2 instance representing the canonical URL of the
727      * currently executing PHP script.
728      * 
729      * @return  string
730      */
731     public static function getCanonical()
732     {
733         if (!isset($_SERVER['REQUEST_METHOD'])) {
734             // ALERT - no current URL
735             throw new Exception('Script was not called through a webserver');
736         }
737
738         // Begin with a relative URL
739         $url = new self($_SERVER['PHP_SELF']);
740         $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
741         $url->host = $_SERVER['SERVER_NAME'];
742         $port = intval($_SERVER['SERVER_PORT']);
743         if ($url->scheme == 'http' && $port != 80 ||
744             $url->scheme == 'https' && $port != 443) {
745
746             $url->port = $port;
747         }
748         return $url;
749     }
750
751     /**
752      * Returns the URL used to retrieve the current request.
753      *
754      * @return  string
755      */
756     public static function getRequestedURL()
757     {
758         return self::getRequested()->getUrl();
759     }
760
761     /**
762      * Returns a Net_URL2 instance representing the URL used to retrieve the
763      * current request.
764      *
765      * @return  Net_URL2
766      */
767     public static function getRequested()
768     {
769         if (!isset($_SERVER['REQUEST_METHOD'])) {
770             // ALERT - no current URL
771             throw new Exception('Script was not called through a webserver');
772         }
773
774         // Begin with a relative URL
775         $url = new self($_SERVER['REQUEST_URI']);
776         $url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
777         // Set host and possibly port
778         $url->setAuthority($_SERVER['HTTP_HOST']);
779         return $url;
780     }
781
782     /**
783      * Sets the specified option.
784      *
785      * @param string $optionName a self::OPTION_ constant
786      * @param mixed  $value      option value  
787      *
788      * @return void
789      * @see  self::OPTION_STRICT
790      * @see  self::OPTION_USE_BRACKETS
791      * @see  self::OPTION_ENCODE_KEYS
792      */
793     function setOption($optionName, $value)
794     {
795         if (!array_key_exists($optionName, $this->options)) {
796             return false;
797         }
798         $this->options[$optionName] = $value;
799     }
800
801     /**
802      * Returns the value of the specified option.
803      *
804      * @param string $optionName The name of the option to retrieve
805      *
806      * @return  mixed
807      */
808     function getOption($optionName)
809     {
810         return isset($this->options[$optionName])
811             ? $this->options[$optionName] : false;
812     }
813 }