]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/Requests/Requests/IRI.php
Add Requests 1.6
[Github/YOURLS.git] / includes / Requests / Requests / IRI.php
1 <?php
2 /**
3  * IRI parser/serialiser/normaliser
4  *
5  * @package Requests
6  * @subpackage Utilities
7  */
8
9 /**
10  * IRI parser/serialiser/normaliser
11  *
12  * Copyright (c) 2007-2010, Geoffrey Sneddon and Steve Minutillo.
13  * All rights reserved.
14  *
15  * Redistribution and use in source and binary forms, with or without
16  * modification, are permitted provided that the following conditions are met:
17  *
18  *  * Redistributions of source code must retain the above copyright notice,
19  *       this list of conditions and the following disclaimer.
20  *
21  *  * Redistributions in binary form must reproduce the above copyright notice,
22  *       this list of conditions and the following disclaimer in the documentation
23  *       and/or other materials provided with the distribution.
24  *
25  *  * Neither the name of the SimplePie Team nor the names of its contributors
26  *       may be used to endorse or promote products derived from this software
27  *       without specific prior written permission.
28  *
29  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
30  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
31  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
32  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
33  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
34  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
35  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
36  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
37  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
38  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39  * POSSIBILITY OF SUCH DAMAGE.
40  *
41  * @package Requests
42  * @subpackage Utilities
43  * @author Geoffrey Sneddon
44  * @author Steve Minutillo
45  * @copyright 2007-2009 Geoffrey Sneddon and Steve Minutillo
46  * @license http://www.opensource.org/licenses/bsd-license.php
47  * @link http://hg.gsnedders.com/iri/
48  */
49 class Requests_IRI
50 {
51     /**
52      * Scheme
53      *
54      * @var string
55      */
56     protected $scheme = null;
57
58     /**
59      * User Information
60      *
61      * @var string
62      */
63     protected $iuserinfo = null;
64
65     /**
66      * ihost
67      *
68      * @var string
69      */
70     protected $ihost = null;
71
72     /**
73      * Port
74      *
75      * @var string
76      */
77     protected $port = null;
78
79     /**
80      * ipath
81      *
82      * @var string
83      */
84     protected $ipath = '';
85
86     /**
87      * iquery
88      *
89      * @var string
90      */
91     protected $iquery = null;
92
93     /**
94      * ifragment
95      *
96      * @var string
97      */
98     protected $ifragment = null;
99
100     /**
101      * Normalization database
102      *
103      * Each key is the scheme, each value is an array with each key as the IRI
104      * part and value as the default value for that part.
105      */
106     protected $normalization = array(
107         'acap' => array(
108             'port' => 674
109         ),
110         'dict' => array(
111             'port' => 2628
112         ),
113         'file' => array(
114             'ihost' => 'localhost'
115         ),
116         'http' => array(
117             'port' => 80,
118             'ipath' => '/'
119         ),
120         'https' => array(
121             'port' => 443,
122             'ipath' => '/'
123         ),
124     );
125
126     /**
127      * Return the entire IRI when you try and read the object as a string
128      *
129      * @return string
130      */
131     public function __toString()
132     {
133         return $this->get_iri();
134     }
135
136     /**
137      * Overload __set() to provide access via properties
138      *
139      * @param string $name Property name
140      * @param mixed $value Property value
141      */
142     public function __set($name, $value)
143     {
144         if (method_exists($this, 'set_' . $name))
145         {
146             call_user_func(array($this, 'set_' . $name), $value);
147         }
148         elseif (
149                $name === 'iauthority'
150             || $name === 'iuserinfo'
151             || $name === 'ihost'
152             || $name === 'ipath'
153             || $name === 'iquery'
154             || $name === 'ifragment'
155         )
156         {
157             call_user_func(array($this, 'set_' . substr($name, 1)), $value);
158         }
159     }
160
161     /**
162      * Overload __get() to provide access via properties
163      *
164      * @param string $name Property name
165      * @return mixed
166      */
167     public function __get($name)
168     {
169         // isset() returns false for null, we don't want to do that
170         // Also why we use array_key_exists below instead of isset()
171         $props = get_object_vars($this);
172
173         if (
174             $name === 'iri' ||
175             $name === 'uri' ||
176             $name === 'iauthority' ||
177             $name === 'authority'
178         )
179         {
180             $return = $this->{"get_$name"}();
181         }
182         elseif (array_key_exists($name, $props))
183         {
184             $return = $this->$name;
185         }
186         // host -> ihost
187         elseif (($prop = 'i' . $name) && array_key_exists($prop, $props))
188         {
189             $name = $prop;
190             $return = $this->$prop;
191         }
192         // ischeme -> scheme
193         elseif (($prop = substr($name, 1)) && array_key_exists($prop, $props))
194         {
195             $name = $prop;
196             $return = $this->$prop;
197         }
198         else
199         {
200             trigger_error('Undefined property: ' . get_class($this) . '::' . $name, E_USER_NOTICE);
201             $return = null;
202         }
203
204         if ($return === null && isset($this->normalization[$this->scheme][$name]))
205         {
206             return $this->normalization[$this->scheme][$name];
207         }
208         else
209         {
210             return $return;
211         }
212     }
213
214     /**
215      * Overload __isset() to provide access via properties
216      *
217      * @param string $name Property name
218      * @return bool
219      */
220     public function __isset($name)
221     {
222         if (method_exists($this, 'get_' . $name) || isset($this->$name))
223         {
224             return true;
225         }
226         else
227         {
228             return false;
229         }
230     }
231
232     /**
233      * Overload __unset() to provide access via properties
234      *
235      * @param string $name Property name
236      */
237     public function __unset($name)
238     {
239         if (method_exists($this, 'set_' . $name))
240         {
241             call_user_func(array($this, 'set_' . $name), '');
242         }
243     }
244
245     /**
246      * Create a new IRI object, from a specified string
247      *
248      * @param string $iri
249      */
250     public function __construct($iri = null)
251     {
252         $this->set_iri($iri);
253     }
254
255     /**
256      * Create a new IRI object by resolving a relative IRI
257      *
258      * Returns false if $base is not absolute, otherwise an IRI.
259      *
260      * @param IRI|string $base (Absolute) Base IRI
261      * @param IRI|string $relative Relative IRI
262      * @return IRI|false
263      */
264     public static function absolutize($base, $relative)
265     {
266         if (!($relative instanceof Requests_IRI))
267         {
268             $relative = new Requests_IRI($relative);
269         }
270         if (!$relative->is_valid())
271         {
272             return false;
273         }
274         elseif ($relative->scheme !== null)
275         {
276             return clone $relative;
277         }
278         else
279         {
280             if (!($base instanceof Requests_IRI))
281             {
282                 $base = new Requests_IRI($base);
283             }
284             if ($base->scheme !== null && $base->is_valid())
285             {
286                 if ($relative->get_iri() !== '')
287                 {
288                     if ($relative->iuserinfo !== null || $relative->ihost !== null || $relative->port !== null)
289                     {
290                         $target = clone $relative;
291                         $target->scheme = $base->scheme;
292                     }
293                     else
294                     {
295                         $target = new Requests_IRI;
296                         $target->scheme = $base->scheme;
297                         $target->iuserinfo = $base->iuserinfo;
298                         $target->ihost = $base->ihost;
299                         $target->port = $base->port;
300                         if ($relative->ipath !== '')
301                         {
302                             if ($relative->ipath[0] === '/')
303                             {
304                                 $target->ipath = $relative->ipath;
305                             }
306                             elseif (($base->iuserinfo !== null || $base->ihost !== null || $base->port !== null) && $base->ipath === '')
307                             {
308                                 $target->ipath = '/' . $relative->ipath;
309                             }
310                             elseif (($last_segment = strrpos($base->ipath, '/')) !== false)
311                             {
312                                 $target->ipath = substr($base->ipath, 0, $last_segment + 1) . $relative->ipath;
313                             }
314                             else
315                             {
316                                 $target->ipath = $relative->ipath;
317                             }
318                             $target->ipath = $target->remove_dot_segments($target->ipath);
319                             $target->iquery = $relative->iquery;
320                         }
321                         else
322                         {
323                             $target->ipath = $base->ipath;
324                             if ($relative->iquery !== null)
325                             {
326                                 $target->iquery = $relative->iquery;
327                             }
328                             elseif ($base->iquery !== null)
329                             {
330                                 $target->iquery = $base->iquery;
331                             }
332                         }
333                         $target->ifragment = $relative->ifragment;
334                     }
335                 }
336                 else
337                 {
338                     $target = clone $base;
339                     $target->ifragment = null;
340                 }
341                 $target->scheme_normalization();
342                 return $target;
343             }
344             else
345             {
346                 return false;
347             }
348         }
349     }
350
351     /**
352      * Parse an IRI into scheme/authority/path/query/fragment segments
353      *
354      * @param string $iri
355      * @return array
356      */
357     protected function parse_iri($iri)
358     {
359         $iri = trim($iri, "\x20\x09\x0A\x0C\x0D");
360         if (preg_match('/^((?P<scheme>[^:\/?#]+):)?(\/\/(?P<authority>[^\/?#]*))?(?P<path>[^?#]*)(\?(?P<query>[^#]*))?(#(?P<fragment>.*))?$/', $iri, $match))
361         {
362             if ($match[1] === '')
363             {
364                 $match['scheme'] = null;
365             }
366             if (!isset($match[3]) || $match[3] === '')
367             {
368                 $match['authority'] = null;
369             }
370             if (!isset($match[5]))
371             {
372                 $match['path'] = '';
373             }
374             if (!isset($match[6]) || $match[6] === '')
375             {
376                 $match['query'] = null;
377             }
378             if (!isset($match[8]) || $match[8] === '')
379             {
380                 $match['fragment'] = null;
381             }
382             return $match;
383         }
384         else
385         {
386             trigger_error('This should never happen', E_USER_ERROR);
387             die;
388         }
389     }
390
391     /**
392      * Remove dot segments from a path
393      *
394      * @param string $input
395      * @return string
396      */
397     protected function remove_dot_segments($input)
398     {
399         $output = '';
400         while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..')
401         {
402             // A: If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise,
403             if (strpos($input, '../') === 0)
404             {
405                 $input = substr($input, 3);
406             }
407             elseif (strpos($input, './') === 0)
408             {
409                 $input = substr($input, 2);
410             }
411             // B: if the input buffer begins with a prefix of "/./" or "/.", where "." is a complete path segment, then replace that prefix with "/" in the input buffer; otherwise,
412             elseif (strpos($input, '/./') === 0)
413             {
414                 $input = substr($input, 2);
415             }
416             elseif ($input === '/.')
417             {
418                 $input = '/';
419             }
420             // C: if the input buffer begins with a prefix of "/../" or "/..", where ".." is a complete path segment, then replace that prefix with "/" in the input buffer and remove the last segment and its preceding "/" (if any) from the output buffer; otherwise,
421             elseif (strpos($input, '/../') === 0)
422             {
423                 $input = substr($input, 3);
424                 $output = substr_replace($output, '', strrpos($output, '/'));
425             }
426             elseif ($input === '/..')
427             {
428                 $input = '/';
429                 $output = substr_replace($output, '', strrpos($output, '/'));
430             }
431             // D: if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise,
432             elseif ($input === '.' || $input === '..')
433             {
434                 $input = '';
435             }
436             // E: move the first path segment in the input buffer to the end of the output buffer, including the initial "/" character (if any) and any subsequent characters up to, but not including, the next "/" character or the end of the input buffer
437             elseif (($pos = strpos($input, '/', 1)) !== false)
438             {
439                 $output .= substr($input, 0, $pos);
440                 $input = substr_replace($input, '', 0, $pos);
441             }
442             else
443             {
444                 $output .= $input;
445                 $input = '';
446             }
447         }
448         return $output . $input;
449     }
450
451     /**
452      * Replace invalid character with percent encoding
453      *
454      * @param string $string Input string
455      * @param string $extra_chars Valid characters not in iunreserved or
456      *                            iprivate (this is ASCII-only)
457      * @param bool $iprivate Allow iprivate
458      * @return string
459      */
460     protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false)
461     {
462         // Normalize as many pct-encoded sections as possible
463         $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', array(&$this, 'remove_iunreserved_percent_encoded'), $string);
464
465         // Replace invalid percent characters
466         $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string);
467
468         // Add unreserved and % to $extra_chars (the latter is safe because all
469         // pct-encoded sections are now valid).
470         $extra_chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~%';
471
472         // Now replace any bytes that aren't allowed with their pct-encoded versions
473         $position = 0;
474         $strlen = strlen($string);
475         while (($position += strspn($string, $extra_chars, $position)) < $strlen)
476         {
477             $value = ord($string[$position]);
478
479             // Start position
480             $start = $position;
481
482             // By default we are valid
483             $valid = true;
484
485             // No one byte sequences are valid due to the while.
486             // Two byte sequence:
487             if (($value & 0xE0) === 0xC0)
488             {
489                 $character = ($value & 0x1F) << 6;
490                 $length = 2;
491                 $remaining = 1;
492             }
493             // Three byte sequence:
494             elseif (($value & 0xF0) === 0xE0)
495             {
496                 $character = ($value & 0x0F) << 12;
497                 $length = 3;
498                 $remaining = 2;
499             }
500             // Four byte sequence:
501             elseif (($value & 0xF8) === 0xF0)
502             {
503                 $character = ($value & 0x07) << 18;
504                 $length = 4;
505                 $remaining = 3;
506             }
507             // Invalid byte:
508             else
509             {
510                 $valid = false;
511                 $length = 1;
512                 $remaining = 0;
513             }
514
515             if ($remaining)
516             {
517                 if ($position + $length <= $strlen)
518                 {
519                     for ($position++; $remaining; $position++)
520                     {
521                         $value = ord($string[$position]);
522
523                         // Check that the byte is valid, then add it to the character:
524                         if (($value & 0xC0) === 0x80)
525                         {
526                             $character |= ($value & 0x3F) << (--$remaining * 6);
527                         }
528                         // If it is invalid, count the sequence as invalid and reprocess the current byte:
529                         else
530                         {
531                             $valid = false;
532                             $position--;
533                             break;
534                         }
535                     }
536                 }
537                 else
538                 {
539                     $position = $strlen - 1;
540                     $valid = false;
541                 }
542             }
543
544             // Percent encode anything invalid or not in ucschar
545             if (
546                 // Invalid sequences
547                 !$valid
548                 // Non-shortest form sequences are invalid
549                 || $length > 1 && $character <= 0x7F
550                 || $length > 2 && $character <= 0x7FF
551                 || $length > 3 && $character <= 0xFFFF
552                 // Outside of range of ucschar codepoints
553                 // Noncharacters
554                 || ($character & 0xFFFE) === 0xFFFE
555                 || $character >= 0xFDD0 && $character <= 0xFDEF
556                 || (
557                     // Everything else not in ucschar
558                        $character > 0xD7FF && $character < 0xF900
559                     || $character < 0xA0
560                     || $character > 0xEFFFD
561                 )
562                 && (
563                     // Everything not in iprivate, if it applies
564                        !$iprivate
565                     || $character < 0xE000
566                     || $character > 0x10FFFD
567                 )
568             )
569             {
570                 // If we were a character, pretend we weren't, but rather an error.
571                 if ($valid)
572                     $position--;
573
574                 for ($j = $start; $j <= $position; $j++)
575                 {
576                     $string = substr_replace($string, sprintf('%%%02X', ord($string[$j])), $j, 1);
577                     $j += 2;
578                     $position += 2;
579                     $strlen += 2;
580                 }
581             }
582         }
583
584         return $string;
585     }
586
587     /**
588      * Callback function for preg_replace_callback.
589      *
590      * Removes sequences of percent encoded bytes that represent UTF-8
591      * encoded characters in iunreserved
592      *
593      * @param array $match PCRE match
594      * @return string Replacement
595      */
596     protected function remove_iunreserved_percent_encoded($match)
597     {
598         // As we just have valid percent encoded sequences we can just explode
599         // and ignore the first member of the returned array (an empty string).
600         $bytes = explode('%', $match[0]);
601
602         // Initialize the new string (this is what will be returned) and that
603         // there are no bytes remaining in the current sequence (unsurprising
604         // at the first byte!).
605         $string = '';
606         $remaining = 0;
607
608         // Loop over each and every byte, and set $value to its value
609         for ($i = 1, $len = count($bytes); $i < $len; $i++)
610         {
611             $value = hexdec($bytes[$i]);
612
613             // If we're the first byte of sequence:
614             if (!$remaining)
615             {
616                 // Start position
617                 $start = $i;
618
619                 // By default we are valid
620                 $valid = true;
621
622                 // One byte sequence:
623                 if ($value <= 0x7F)
624                 {
625                     $character = $value;
626                     $length = 1;
627                 }
628                 // Two byte sequence:
629                 elseif (($value & 0xE0) === 0xC0)
630                 {
631                     $character = ($value & 0x1F) << 6;
632                     $length = 2;
633                     $remaining = 1;
634                 }
635                 // Three byte sequence:
636                 elseif (($value & 0xF0) === 0xE0)
637                 {
638                     $character = ($value & 0x0F) << 12;
639                     $length = 3;
640                     $remaining = 2;
641                 }
642                 // Four byte sequence:
643                 elseif (($value & 0xF8) === 0xF0)
644                 {
645                     $character = ($value & 0x07) << 18;
646                     $length = 4;
647                     $remaining = 3;
648                 }
649                 // Invalid byte:
650                 else
651                 {
652                     $valid = false;
653                     $remaining = 0;
654                 }
655             }
656             // Continuation byte:
657             else
658             {
659                 // Check that the byte is valid, then add it to the character:
660                 if (($value & 0xC0) === 0x80)
661                 {
662                     $remaining--;
663                     $character |= ($value & 0x3F) << ($remaining * 6);
664                 }
665                 // If it is invalid, count the sequence as invalid and reprocess the current byte as the start of a sequence:
666                 else
667                 {
668                     $valid = false;
669                     $remaining = 0;
670                     $i--;
671                 }
672             }
673
674             // If we've reached the end of the current byte sequence, append it to Unicode::$data
675             if (!$remaining)
676             {
677                 // Percent encode anything invalid or not in iunreserved
678                 if (
679                     // Invalid sequences
680                     !$valid
681                     // Non-shortest form sequences are invalid
682                     || $length > 1 && $character <= 0x7F
683                     || $length > 2 && $character <= 0x7FF
684                     || $length > 3 && $character <= 0xFFFF
685                     // Outside of range of iunreserved codepoints
686                     || $character < 0x2D
687                     || $character > 0xEFFFD
688                     // Noncharacters
689                     || ($character & 0xFFFE) === 0xFFFE
690                     || $character >= 0xFDD0 && $character <= 0xFDEF
691                     // Everything else not in iunreserved (this is all BMP)
692                     || $character === 0x2F
693                     || $character > 0x39 && $character < 0x41
694                     || $character > 0x5A && $character < 0x61
695                     || $character > 0x7A && $character < 0x7E
696                     || $character > 0x7E && $character < 0xA0
697                     || $character > 0xD7FF && $character < 0xF900
698                 )
699                 {
700                     for ($j = $start; $j <= $i; $j++)
701                     {
702                         $string .= '%' . strtoupper($bytes[$j]);
703                     }
704                 }
705                 else
706                 {
707                     for ($j = $start; $j <= $i; $j++)
708                     {
709                         $string .= chr(hexdec($bytes[$j]));
710                     }
711                 }
712             }
713         }
714
715         // If we have any bytes left over they are invalid (i.e., we are
716         // mid-way through a multi-byte sequence)
717         if ($remaining)
718         {
719             for ($j = $start; $j < $len; $j++)
720             {
721                 $string .= '%' . strtoupper($bytes[$j]);
722             }
723         }
724
725         return $string;
726     }
727
728     protected function scheme_normalization()
729     {
730         if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo'])
731         {
732             $this->iuserinfo = null;
733         }
734         if (isset($this->normalization[$this->scheme]['ihost']) && $this->ihost === $this->normalization[$this->scheme]['ihost'])
735         {
736             $this->ihost = null;
737         }
738         if (isset($this->normalization[$this->scheme]['port']) && $this->port === $this->normalization[$this->scheme]['port'])
739         {
740             $this->port = null;
741         }
742         if (isset($this->normalization[$this->scheme]['ipath']) && $this->ipath === $this->normalization[$this->scheme]['ipath'])
743         {
744             $this->ipath = '';
745         }
746         if (isset($this->normalization[$this->scheme]['iquery']) && $this->iquery === $this->normalization[$this->scheme]['iquery'])
747         {
748             $this->iquery = null;
749         }
750         if (isset($this->normalization[$this->scheme]['ifragment']) && $this->ifragment === $this->normalization[$this->scheme]['ifragment'])
751         {
752             $this->ifragment = null;
753         }
754     }
755
756     /**
757      * Check if the object represents a valid IRI. This needs to be done on each
758      * call as some things change depending on another part of the IRI.
759      *
760      * @return bool
761      */
762     public function is_valid()
763     {
764         $isauthority = $this->iuserinfo !== null || $this->ihost !== null || $this->port !== null;
765         if ($this->ipath !== '' &&
766             (
767                 $isauthority && (
768                     $this->ipath[0] !== '/' ||
769                     substr($this->ipath, 0, 2) === '//'
770                 ) ||
771                 (
772                     $this->scheme === null &&
773                     !$isauthority &&
774                     strpos($this->ipath, ':') !== false &&
775                     (strpos($this->ipath, '/') === false ? true : strpos($this->ipath, ':') < strpos($this->ipath, '/'))
776                 )
777             )
778         )
779         {
780             return false;
781         }
782
783         return true;
784     }
785
786     /**
787      * Set the entire IRI. Returns true on success, false on failure (if there
788      * are any invalid characters).
789      *
790      * @param string $iri
791      * @return bool
792      */
793     protected function set_iri($iri)
794     {
795         static $cache;
796         if (!$cache)
797         {
798             $cache = array();
799         }
800
801         if ($iri === null)
802         {
803             return true;
804         }
805         elseif (isset($cache[$iri]))
806         {
807             list($this->scheme,
808                  $this->iuserinfo,
809                  $this->ihost,
810                  $this->port,
811                  $this->ipath,
812                  $this->iquery,
813                  $this->ifragment,
814                  $return) = $cache[$iri];
815             return $return;
816         }
817         else
818         {
819             $parsed = $this->parse_iri((string) $iri);
820
821             $return = $this->set_scheme($parsed['scheme'])
822                 && $this->set_authority($parsed['authority'])
823                 && $this->set_path($parsed['path'])
824                 && $this->set_query($parsed['query'])
825                 && $this->set_fragment($parsed['fragment']);
826
827             $cache[$iri] = array($this->scheme,
828                                  $this->iuserinfo,
829                                  $this->ihost,
830                                  $this->port,
831                                  $this->ipath,
832                                  $this->iquery,
833                                  $this->ifragment,
834                                  $return);
835             return $return;
836         }
837     }
838
839     /**
840      * Set the scheme. Returns true on success, false on failure (if there are
841      * any invalid characters).
842      *
843      * @param string $scheme
844      * @return bool
845      */
846     protected function set_scheme($scheme)
847     {
848         if ($scheme === null)
849         {
850             $this->scheme = null;
851         }
852         elseif (!preg_match('/^[A-Za-z][0-9A-Za-z+\-.]*$/', $scheme))
853         {
854             $this->scheme = null;
855             return false;
856         }
857         else
858         {
859             $this->scheme = strtolower($scheme);
860         }
861         return true;
862     }
863
864     /**
865      * Set the authority. Returns true on success, false on failure (if there are
866      * any invalid characters).
867      *
868      * @param string $authority
869      * @return bool
870      */
871     protected function set_authority($authority)
872     {
873         static $cache;
874         if (!$cache)
875             $cache = array();
876
877         if ($authority === null)
878         {
879             $this->iuserinfo = null;
880             $this->ihost = null;
881             $this->port = null;
882             return true;
883         }
884         elseif (isset($cache[$authority]))
885         {
886             list($this->iuserinfo,
887                  $this->ihost,
888                  $this->port,
889                  $return) = $cache[$authority];
890
891             return $return;
892         }
893         else
894         {
895             $remaining = $authority;
896             if (($iuserinfo_end = strrpos($remaining, '@')) !== false)
897             {
898                 $iuserinfo = substr($remaining, 0, $iuserinfo_end);
899                 $remaining = substr($remaining, $iuserinfo_end + 1);
900             }
901             else
902             {
903                 $iuserinfo = null;
904             }
905             if (($port_start = strpos($remaining, ':', strpos($remaining, ']'))) !== false)
906             {
907                 if (($port = substr($remaining, $port_start + 1)) === false)
908                 {
909                     $port = null;
910                 }
911                 $remaining = substr($remaining, 0, $port_start);
912             }
913             else
914             {
915                 $port = null;
916             }
917
918             $return = $this->set_userinfo($iuserinfo) &&
919                       $this->set_host($remaining) &&
920                       $this->set_port($port);
921
922             $cache[$authority] = array($this->iuserinfo,
923                                        $this->ihost,
924                                        $this->port,
925                                        $return);
926
927             return $return;
928         }
929     }
930
931     /**
932      * Set the iuserinfo.
933      *
934      * @param string $iuserinfo
935      * @return bool
936      */
937     protected function set_userinfo($iuserinfo)
938     {
939         if ($iuserinfo === null)
940         {
941             $this->iuserinfo = null;
942         }
943         else
944         {
945             $this->iuserinfo = $this->replace_invalid_with_pct_encoding($iuserinfo, '!$&\'()*+,;=:');
946             $this->scheme_normalization();
947         }
948
949         return true;
950     }
951
952     /**
953      * Set the ihost. Returns true on success, false on failure (if there are
954      * any invalid characters).
955      *
956      * @param string $ihost
957      * @return bool
958      */
959     protected function set_host($ihost)
960     {
961         if ($ihost === null)
962         {
963             $this->ihost = null;
964             return true;
965         }
966         elseif (substr($ihost, 0, 1) === '[' && substr($ihost, -1) === ']')
967         {
968             if (Requests_IPv6::check_ipv6(substr($ihost, 1, -1)))
969             {
970                 $this->ihost = '[' . Requests_IPv6::compress(substr($ihost, 1, -1)) . ']';
971             }
972             else
973             {
974                 $this->ihost = null;
975                 return false;
976             }
977         }
978         else
979         {
980             $ihost = $this->replace_invalid_with_pct_encoding($ihost, '!$&\'()*+,;=');
981
982             // Lowercase, but ignore pct-encoded sections (as they should
983             // remain uppercase). This must be done after the previous step
984             // as that can add unescaped characters.
985             $position = 0;
986             $strlen = strlen($ihost);
987             while (($position += strcspn($ihost, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ%', $position)) < $strlen)
988             {
989                 if ($ihost[$position] === '%')
990                 {
991                     $position += 3;
992                 }
993                 else
994                 {
995                     $ihost[$position] = strtolower($ihost[$position]);
996                     $position++;
997                 }
998             }
999
1000             $this->ihost = $ihost;
1001         }
1002
1003         $this->scheme_normalization();
1004
1005         return true;
1006     }
1007
1008     /**
1009      * Set the port. Returns true on success, false on failure (if there are
1010      * any invalid characters).
1011      *
1012      * @param string $port
1013      * @return bool
1014      */
1015     protected function set_port($port)
1016     {
1017         if ($port === null)
1018         {
1019             $this->port = null;
1020             return true;
1021         }
1022         elseif (strspn($port, '0123456789') === strlen($port))
1023         {
1024             $this->port = (int) $port;
1025             $this->scheme_normalization();
1026             return true;
1027         }
1028         else
1029         {
1030             $this->port = null;
1031             return false;
1032         }
1033     }
1034
1035     /**
1036      * Set the ipath.
1037      *
1038      * @param string $ipath
1039      * @return bool
1040      */
1041     protected function set_path($ipath)
1042     {
1043         static $cache;
1044         if (!$cache)
1045         {
1046             $cache = array();
1047         }
1048
1049         $ipath = (string) $ipath;
1050
1051         if (isset($cache[$ipath]))
1052         {
1053             $this->ipath = $cache[$ipath][(int) ($this->scheme !== null)];
1054         }
1055         else
1056         {
1057             $valid = $this->replace_invalid_with_pct_encoding($ipath, '!$&\'()*+,;=@:/');
1058             $removed = $this->remove_dot_segments($valid);
1059
1060             $cache[$ipath] = array($valid, $removed);
1061             $this->ipath =  ($this->scheme !== null) ? $removed : $valid;
1062         }
1063
1064         $this->scheme_normalization();
1065         return true;
1066     }
1067
1068     /**
1069      * Set the iquery.
1070      *
1071      * @param string $iquery
1072      * @return bool
1073      */
1074     protected function set_query($iquery)
1075     {
1076         if ($iquery === null)
1077         {
1078             $this->iquery = null;
1079         }
1080         else
1081         {
1082             $this->iquery = $this->replace_invalid_with_pct_encoding($iquery, '!$&\'()*+,;=:@/?', true);
1083             $this->scheme_normalization();
1084         }
1085         return true;
1086     }
1087
1088     /**
1089      * Set the ifragment.
1090      *
1091      * @param string $ifragment
1092      * @return bool
1093      */
1094     protected function set_fragment($ifragment)
1095     {
1096         if ($ifragment === null)
1097         {
1098             $this->ifragment = null;
1099         }
1100         else
1101         {
1102             $this->ifragment = $this->replace_invalid_with_pct_encoding($ifragment, '!$&\'()*+,;=:@/?');
1103             $this->scheme_normalization();
1104         }
1105         return true;
1106     }
1107
1108     /**
1109      * Convert an IRI to a URI (or parts thereof)
1110      *
1111      * @return string
1112      */
1113     protected function to_uri($string)
1114     {
1115         static $non_ascii;
1116         if (!$non_ascii)
1117         {
1118             $non_ascii = implode('', range("\x80", "\xFF"));
1119         }
1120
1121         $position = 0;
1122         $strlen = strlen($string);
1123         while (($position += strcspn($string, $non_ascii, $position)) < $strlen)
1124         {
1125             $string = substr_replace($string, sprintf('%%%02X', ord($string[$position])), $position, 1);
1126             $position += 3;
1127             $strlen += 2;
1128         }
1129
1130         return $string;
1131     }
1132
1133     /**
1134      * Get the complete IRI
1135      *
1136      * @return string
1137      */
1138     protected function get_iri()
1139     {
1140         if (!$this->is_valid())
1141         {
1142             return false;
1143         }
1144
1145         $iri = '';
1146         if ($this->scheme !== null)
1147         {
1148             $iri .= $this->scheme . ':';
1149         }
1150         if (($iauthority = $this->get_iauthority()) !== null)
1151         {
1152             $iri .= '//' . $iauthority;
1153         }
1154         $iri .= $this->ipath;
1155         if ($this->iquery !== null)
1156         {
1157             $iri .= '?' . $this->iquery;
1158         }
1159         if ($this->ifragment !== null)
1160         {
1161             $iri .= '#' . $this->ifragment;
1162         }
1163
1164         return $iri;
1165     }
1166
1167     /**
1168      * Get the complete URI
1169      *
1170      * @return string
1171      */
1172     protected function get_uri()
1173     {
1174         return $this->to_uri($this->get_iri());
1175     }
1176
1177     /**
1178      * Get the complete iauthority
1179      *
1180      * @return string
1181      */
1182     protected function get_iauthority()
1183     {
1184         if ($this->iuserinfo !== null || $this->ihost !== null || $this->port !== null)
1185         {
1186             $iauthority = '';
1187             if ($this->iuserinfo !== null)
1188             {
1189                 $iauthority .= $this->iuserinfo . '@';
1190             }
1191             if ($this->ihost !== null)
1192             {
1193                 $iauthority .= $this->ihost;
1194             }
1195             if ($this->port !== null)
1196             {
1197                 $iauthority .= ':' . $this->port;
1198             }
1199             return $iauthority;
1200         }
1201         else
1202         {
1203             return null;
1204         }
1205     }
1206
1207     /**
1208      * Get the complete authority
1209      *
1210      * @return string
1211      */
1212     protected function get_authority()
1213     {
1214         $iauthority = $this->get_iauthority();
1215         if (is_string($iauthority))
1216             return $this->to_uri($iauthority);
1217         else
1218             return $iauthority;
1219     }
1220 }