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